AIGC标识 [深入解析C#] 第 14 章:C# 7 的代码简洁之道

14.1 局部方法

  • 核心概念:在方法内部定义的方法,允许将仅在单一方法中使用的辅助逻辑封装在局部作用域,共享外部变量,避免代码重复和额外类成员。

  • 关键点

    • 基本特性
      • 声明在任何可包含语句的成员中(方法、构造器、属性、索引器、事件、终结器等),甚至可以嵌套。
      • 可以访问和修改外部方法中的局部变量(闭包捕获)。
      • 声明顺序灵活,可以在调用位置之后定义。
    • 限制(相比普通方法)
      • 不能有访问修饰符(public/private 等)。
      • 不能是 externvirtualnewoverridestaticabstract
      • 不能应用 attribute(如 [MethodImpl])。
      • 同一作用域内不允许有同名的局部方法,不支持方法重载。
    • 与普通方法相同的特性
      • 可以有返回值或 void
      • 可以是 asyncunsafe
      • 可以实现为迭代器块(yield return)。
      • 可以有参数(包括可选参数和 params)。
      • 可以是泛型方法,并能使用闭合类型形参。
      • 可以作为委托的目标(方法组转换)。
    • 闭包实现差异(相对于 lambda):
      • 局部方法在 IL 层面通常实现为普通方法(若不需要捕获变量),或结构体/类闭包,编译器可选择更高效的分配方式(可能堆分配更少),性能优于 delegate/lambda。
    • 典型应用
      • 提取重复的辅助逻辑,尤其是需要在多处调用并修改外部变量的场景。
      • 实现递归辅助方法(如树遍历),而无需暴露给外部。
      • 在迭代器块中进行参数验证(因为迭代器代码会被延迟执行,直接在原方法体开头判断参数并抛出异常时,局部方法可以用来立即执行校验)。
  • 代码示例

    static void Main()
    {
        int x = 10;
        PrintAndIncrementX();
        PrintAndIncrementX();
        Console.WriteLine($"x = {x}"); // x = 12
    
        void PrintAndIncrementX()
        {
            Console.WriteLine($"x = {x}");
            x++;
        }
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,常用特性,体现现代 C# 编码风格)
    • 面试回答要点
      • 局部方法是 C# 7 引入的,用于在方法内部定义辅助函数,提升封装性和可读性。
      • 与 lambda 的区别:局部方法在编译时更高效(避免 delegate 分配),且支持迭代器、泛型、可选参数等,更灵活。
      • 典型场景:用于迭代器块内的参数验证(因为迭代器方法中的代码直到第一次 MoveNext 才执行,如果直接在方法开头校验参数则达不到立即验证的效果,可通过一个普通方法包裹,局部方法则更优雅)。
      • 在 Unity 中,可用于复杂的计算逻辑(如寻路算法中的递归辅助方法)、协程内部的小工具函数,避免类的成员膨胀。
      • 注意:不能声明为 static,但 C# 8 起支持 static 局部方法(可阻止意外捕获外部变量),面试时可提及展现版本知识。

14.1.1 局部方法中的变量访问

  • 核心概念:局部方法可以捕获并使用外部变量,但必须遵守严格的作用域、声明顺序和确定赋值规则,且不能捕获 ref 参数或为只读字段赋值。这些规则保证了局部方法在编译时能被安全地展开为普通方法,同时避免生命周期和内存安全问题。

  • 关键点

    • 作用域限制:局部方法只能访问其声明所在代码块内的局部变量。若需要在循环中捕获循环变量,局部方法必须定义在循环体内。
    • 声明顺序要求:局部方法的声明必须位于其所捕获的变量声明之后,否则会编译错误(CS0841)。
    • 不能捕获 ref 参数:局部方法不能直接捕获外部方法的 ref 参数(包括结构体实例方法中的隐含 this,因为结构体的 this 是引用)。如需访问,可通过显式参数以 ref 或值传递方式再次传入。
    • 确定赋值规则
      • 在调用局部方法时,其读取的捕获变量必须已被确定赋值,否则编译错误。
      • 如果局部方法在所有执行路径上对捕获变量赋值,那么调用该方法后,该变量被视为已确定赋值,可在外部方法后续代码中使用。
    • 只读字段限制:即使在构造函数内,局部方法也不能为 readonly 字段赋值(字段初始化只能由构造函数本身直接执行)。
    • 设计初衷:这些规则确保局部方法在 IL 层面可被转换为不带闭包的普通静态方法(若不需要捕获)或高效闭包结构,而不会引入 CLR 层面的新概念。
  • 代码示例

    // 合法:局部方法在变量声明后、在循环内捕获循环变量
    static void ValidLoop()
    {
        for (int i = 0; i < 10; i++)
        {
            PrintI();
            void PrintI() => Console.WriteLine(i);
        }
    }
    
    // 非法:局部方法在变量声明前捕获
    // void PrintI() => Console.WriteLine(i); // CS0841
    // int i = 10;
    
    // 处理 ref 参数的合法方式
    static void ValidRef(ref int p)
    {
        PrintAndIncrement(ref p);
        void PrintAndIncrement(ref int x) => Console.WriteLine(x++);
    }
    
    // 确定赋值示例
    static void DefinitelyAssign()
    {
        int i;
        AssignI();       // 调用后 i 确定赋值
        Console.WriteLine(i); // 合法
        void AssignI() => i = 10;
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现对闭包和编译原理的理解)
    • 面试回答要点
      • 局部方法捕获变量遵循严格的“声明在变量之后”和“作用域内”规则,避免因生命周期混乱导致的 bug。
      • 对比 lambda 表达式:lambda 可捕获任意范围内的变量,但会延长变量生命周期并产生 GC 分配;局部方法编译器行为更优,可能生成值类型闭包或静态方法,性能更好。
      • 不能捕获 ref 参数是由于生命周期和内存安全考虑,可通过显式传递参数解决;结构体中无法捕获 this 同理。
      • 在 Unity 中,当需要在迭代器(如 IEnumerator)或 async 方法内部提取重复逻辑时,局部方法比 lambda 更合适,且能避免额外委托分配。
      • 可提及 C# 8 引入的 static 局部方法,它明确不捕获任何变量,进一步保证意图清晰和性能优化。

14.1.2 局部方法中的变量访问规则

  • 核心概念:局部方法可以捕获并使用外部变量,但必须遵守严格的作用域、声明顺序和确定赋值规则,且不能捕获 ref 参数或为只读字段赋值。这些规则保证了局部方法在编译时能被安全地展开为普通方法,同时避免生命周期和内存安全问题。

  • 关键点

    • 作用域限制:局部方法只能访问其声明所在代码块内的局部变量。若需要在循环中捕获循环变量,局部方法必须定义在循环体内。
    • 声明顺序要求:局部方法的声明必须位于其所捕获的变量声明之后,否则会编译错误(CS0841)。
    • 不能捕获 ref 参数:局部方法不能直接捕获外部方法的 ref 参数(包括结构体实例方法中的隐含 this,因为结构体的 this 是引用)。如需访问,可通过显式参数以 ref 或值传递方式再次传入。
    • 确定赋值规则
      • 在调用局部方法时,其读取的捕获变量必须已被确定赋值,否则编译错误。
      • 如果局部方法在所有执行路径上对捕获变量赋值,那么调用该方法后,该变量被视为已确定赋值,可在外部方法后续代码中使用。
    • 只读字段限制:即使在构造函数内,局部方法也不能为 readonly 字段赋值(字段初始化只能由构造函数本身直接执行)。
    • 设计初衷:这些规则确保局部方法在 IL 层面可被转换为不带闭包的普通静态方法(若不需要捕获)或高效闭包结构,而不会引入 CLR 层面的新概念。
  • 代码示例

    // 合法:局部方法在变量声明后、在循环内捕获循环变量
    static void ValidLoop()
    {
        for (int i = 0; i < 10; i++)
        {
            PrintI();
            void PrintI() => Console.WriteLine(i);
        }
    }
    
    // 非法:局部方法在变量声明前捕获
    // void PrintI() => Console.WriteLine(i); // CS0841
    // int i = 10;
    
    // 处理 ref 参数的合法方式
    static void ValidRef(ref int p)
    {
        PrintAndIncrement(ref p);
        void PrintAndIncrement(ref int x) => Console.WriteLine(x++);
    }
    
    // 确定赋值示例
    static void DefinitelyAssign()
    {
        int i;
        AssignI();       // 调用后 i 确定赋值
        Console.WriteLine(i); // 合法
        void AssignI() => i = 10;
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现对闭包和编译原理的理解)
    • 面试回答要点
      • 局部方法捕获变量遵循严格的“声明在变量之后”和“作用域内”规则,避免因生命周期混乱导致的 bug。
      • 对比 lambda 表达式:lambda 可捕获任意范围内的变量,但会延长变量生命周期并产生 GC 分配;局部方法编译器行为更优,可能生成值类型闭包或静态方法,性能更好。
      • 不能捕获 ref 参数是由于生命周期和内存安全考虑,可通过显式传递参数解决;结构体中无法捕获 this 同理。
      • 在 Unity 中,当需要在迭代器(如 IEnumerator)或 async 方法内部提取重复逻辑时,局部方法比 lambda 更合适,且能避免额外委托分配。
      • 可提及 C# 8 引入的 static 局部方法,它明确不捕获任何变量,进一步保证意图清晰和性能优化。

14.1.3 使用指南

  • 核心概念:局部方法适用于方法内部重复逻辑的抽取,以及仅被单一方法使用的私有辅助方法的内聚。它强调代码属于方法实现细节,而非类型公共接口。

  • 关键点

    • 典型场景
      1. 同一方法内多处重复的逻辑。
      2. 仅在一个方法中使用的私有方法,可将其移入调用处,增强内聚。
    • 重构策略
      • 先将私有方法保持签名不变移到调用方法内部。
      • 再检查其参数:若所有调用都传递的是当前方法的局部变量,可将参数替换为直接捕获变量,甚至完全删除参数。
    • 迭代器/异步方法参数校验优化
      • 问题:带 yieldasync 的方法,内部代码会延迟执行,参数校验若写在方法体内将无法立即生效。
      • 传统方案:拆成两个方法,外层立即校验,内层用 yield/await 实现逻辑。
      • 局部方法方案:外层方法校验参数,局部方法返回迭代器/异步任务并捕获已验证的参数(或显式传递),既保留立即校验,又将实现内聚在一起。
      • 性能提示:显式将参数传给局部方法(而非让局部方法捕获外层变量)可以减少编译器生成的闭包对象,降低堆分配。
    • 可读性建议
      • 特性较新,作者使用谨慎:倾向于只在宿主方法末尾定义局部方法,避免在循环或嵌套块中定义,也不嵌套局部方法。
      • 不要在可读性上过度牺牲,特性应服务于代码清晰度,而非炫技。
  • 代码示例(迭代器参数校验重构对比):

    // 传统分离方法
    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
    {
        Preconditions.CheckNotNull(source, nameof(source));
        Preconditions.CheckNotNull(selector, nameof(selector));
        return SelectImpl(source, selector);
    }
    private static IEnumerable<TResult> SelectImpl<TSource, TResult>(
        IEnumerable<TSource> source, Func<TSource, TResult> selector)
    {
        foreach (TSource item in source)
            yield return selector(item);
    }
    
    // 使用局部方法内聚
    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
    {
        Preconditions.CheckNotNull(source, nameof(source));
        Preconditions.CheckNotNull(selector, nameof(selector));
        return Impl(source, selector); // 显式传参,避免闭包分配
    
        IEnumerable<TResult> Impl(
            IEnumerable<TSource> s, Func<TSource, TResult> sel)
        {
            foreach (TSource item in s)
                yield return sel(item);
        }
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现对代码组织和性能的意识)
    • 面试回答要点
      • 局部方法主要解决重复逻辑抽取和单一用途私有方法的内聚问题,使代码意图更明确。
      • 在迭代器(yield)和 async 方法中,可用外层方法+局部方法模式实现“立即参数校验,延迟逻辑执行”,替代旧的手动拆分私有方法。
      • 性能上,显式给局部方法传参可避免闭包对象分配,比直接捕获外层变量更高效;但要结合实际可读性权衡。
      • 在 Unity 中,编写协程(IEnumerator)时可利用该模式在校验参数后立即抛出异常,而不必等到首次迭代才发现错误,提升调试体验。
      • 风格上应保守使用:优先定义在方法末尾,避免嵌套,确保代码清晰而非炫技。

14.2 out 变量

❗️14.2.1 out 参数的内联变量声明

  • 核心概念:C# 7 允许在方法调用时直接声明 out 参数变量,无需预先声明,从而减少样板代码,使表达式主体和方法链更流畅。

  • 关键点

    • 内联声明语法int.TryParse(text, out int value) —— 变量 value 在调用处声明。
    • 支持隐式类型:可使用 out var 让编译器推断类型。
    • 抛弃变量:用 _ 忽略不需要的 out 值,如 out _
    • 作用域:变量作用域为闭合块(如方法体、if 块等),与模式变量规则一致。
    • 确定赋值规则
      • 若方法在所有路径上都被调用,则 out 变量在调用后确定赋值。
      • 结合 && 短路逻辑时,仅当左侧为 true 才会调用右侧方法,因此右侧 out 变量仅在左侧成功后才确定赋值。
    • 限制
      • 不能用于表达式树。
      • 不能用于字段、属性、构造器初始化器,C# 7.3 之前也不能用于查询表达式。
  • 代码示例

    // 传统写法(C# 6 及之前)
    static int? ParseInt32(string text)
    {
        int value;
        return int.TryParse(text, out value) ? value : (int?) null;
    }
    
    // C# 7 内联声明
    static int? ParseInt32(string text) =>
        int.TryParse(text, out int value) ? value : (int?) null;
    
    // 多个 out 与短路逻辑
    static int? ParseAndSum(string text1, string text2) =>
        int.TryParse(text1, out int value1) &&
        int.TryParse(text2, out int value2)
            ? value1 + value2 : (int?) null;
    // 若第一个 TryParse 失败,value2 未被赋值,不可用
    
  • 面试准备建议

    • 重要性:⭐⭐⭐(高频使用,必须掌握)
    • 面试回答要点
      • out 内联声明简化了方法调用,特别是与 TryParseTryGetValue 等模式结合时,消除额外的变量声明语句。
      • 能解释变量作用域和确定赋值规则:内联变量在调用所在闭合块中可见;仅当方法实际执行时变量才被赋值。
      • 对比旧式写法,指出 C# 7 的改进使表达式主体成员更易实现。
      • 在 Unity 中,常用场景包括:int.TryParseDictionary.TryGetValue、物理射线检测 (Physics.Raycast 使用 out RaycastHit)、Animator.TryGetAnimatorParameter 等,可直接内联声明变量,减少代码行数,提升可读性。
      • 注意:在 &&|| 条件中使用多个 out 变量时,需留意短路导致的未赋值风险。

📦14.2.2 解除 out 变量与模式变量的初始化器限制(C# 7.3)

  • 核心概念:C# 7.3 解除了此前禁止在字段/属性初始化器、构造器初始化器(base/this)以及查询表达式中使用 out 变量和模式变量的限制,使这些变量可在更多上下文中使用。

  • 关键点

    • 解除的限制
      • 允许在构造器初始化器中声明和使用 out 变量(如 : base(text, int.TryParse(text, out int parseResult)))。
      • 构造器体内可以访问该 out 变量(其确定赋值状态取决于方法执行路径)。
      • 同理,模式变量现在也可用于这些上下文。
    • 实际影响
      • 减少了仅为了绕开限制而不得不创建额外方法或临时变量的情况。
      • 让构造器链式调用时可以直接利用 TryParse 等方法的输出,代码更紧凑。
    • 依然存在的限制
      • 表达式树中仍不能使用 out 变量或模式变量。
  • 代码示例

    class ParsedInt32 : ParsedText
    {
        public int? Value { get; }
    
        public ParsedInt32(string text)
            : base(text, int.TryParse(text, out int parseResult)) // C# 7.3 允许
        {
            Value = Valid ? parseResult : (int?) null; // 在体内使用
        }
    }
    
  • 面试准备建议

    • 重要性:⭐(了解即可,属于语言演进的补丁)
    • 面试回答要点
      • C# 7.3 移除了 out 变量和模式变量在初始化器和构造器初始化器中的使用限制,这是对语言一致性的完善。
      • 如果被问及 C# 版本差异,可以此为例子展示细节了解。

14.3 数字字面量的改进

📦14.3.1 二进制整型字面量

  • 核心概念:C# 7 新增 0b / 0B 前缀,用于直接书写二进制整数,使位标志和特定二进制模式更加直观可读。

  • 关键点

    • 语法0b0B 开头,后跟 01 组成的二进制序列。
    • 使用场景:定义位掩码、硬件寄存器、协议标志、枚举组合等需要精确控制二进制的场合。
    • 与十六进制/十进制对比:十进制不直观,十六进制需心算转位模式;二进制直接“所见即所得”。
    • 可读性增强:无需计算或查表即可看出每一位的值。
  • 代码示例

    byte b1 = 135;           // 十进制,不直观
    byte b2 = 0x83;          // 十六进制,需转换
    byte b3 = 0b10000111;    // 二进制,一目了然:第7位和第0,1,2位为1
    
  • 面试准备建议

    • 重要性:⭐(简单特性,了解即可)
    • 面试回答要点
      • 二进制字面量用 0b 前缀,提升位运算代码的可读性。
      • 在 Unity 中,可能用于定义自定义标志枚举(如 [Flags] 枚举)或与某些原生插件交互时的配置值,能以更自然的方式表达位组合。

📦14.3.2 下划线分隔符

  • 核心概念:在数字字面量中可使用下划线 _ 作为分隔符,提升长数字的可读性,不影响数值。适用于十进制、十六进制、二进制及浮点数字面量。

  • 关键点

    • 基本用法1_000_000(十进制)、0xff_00_ff_00(十六进制)、0b1000_0111(二进制)、123_456.78m(小数)。
    • 可读性优势:可按“千分位”、字节、位等逻辑分组,使数值意图一目了然。
    • 灵活性:允许连续多个下划线或非标准分组,编译器不检查分组合理性,但不推荐乱用。
    • 限制
      • 不能出现在字面量开头或结尾(如 _123123_)。
      • 不能出现在小数点前后(如 123_.45123._45)。
      • C# 7.0/7.1 中,0x/0b 前缀后不能紧跟下划线;C# 7.2 解除此限制,允许 0x_ff0b_1000
    • 最佳实践:采用有意义的分组,如 2_147_483_6470b1000_0111,避免无意义的 1________5
  • 代码示例

    int maxInt32 = 2_147_483_647;
    decimal largeSalary = 123_456_789.12m;
    ulong alternatingBytes = 0xff_00_ff_00_ff_00_ff_00;
    ulong alternatingWords = 0xffff_0000_ffff_0000;
    byte flags = 0b1000_0111; // 高位和低四位一目了然
    // C# 7.2 起允许
    ulong also = 0x_ffff_0000;
    byte alsoBin = 0b_1000_0111;
    
  • 面试准备建议

    • 重要性:⭐(简单特性,了解即可)
    • 面试回答要点
      • 下划线分隔符是 C# 7 提供的纯可读性增强,对数值无任何影响。
      • 最适合长整数、位掩码、大金额等需要分组的场景。
      • 在 Unity 中,可用于定义清晰的配置常量,如 const int MaxHealth = 10_000;,提高代码可维护性。

14.4 throw表达式

  • 核心概念:C# 7 允许在特定上下文中将 throw 作为表达式使用,简化条件抛出异常或表达式体成员中的异常逻辑,减少对额外语句的需求。

  • 关键点

    • 允许 throw 表达式的位置
      • 表达式主体成员(如 => throw new ...
      • Lambda 表达式主体(如 x => throw new ...
      • ?? 运算符的右侧操作数
      • ?: 条件运算符的第二或第三操作数(但不能同时)
    • 依然受限的位置
      • 不能单独作为赋值语句(int x = throw ... 无意义)
      • 不能直接用作方法实参(Console.WriteLine(throw ...) 非法)
    • 设计意图:让异常抛出可以与新的表达式语法自然结合,减少纯包装方法或多余的 if-else 块,提升代码紧凑度和可读性。
    • 本质仍是语句:throw 表达式的“结果类型”是任何类型,在需要表达式的地方使用,但它会立即抛出异常,实际上不会返回任何值。
  • 代码示例

    // 表达式主体方法
    public void Unimplemented() => throw new NotImplementedException();
    
    // ?? 运算符
    public static T CheckNotNull<T>(T value, string paramName) where T : class =>
        value ?? throw new ArgumentNullException(paramName);
    
    // ?: 运算符
    public string Name =>
        initialized ? data["name"] : throw new Exception("Not initialized");
    
  • 面试准备建议

    • 重要性:⭐⭐(常用语法糖,提升简洁度)
    • 面试回答要点
      • throw 表达式是 C# 7 的改进,让异常抛出可以无缝融入表达式体语法、空合并运算符和条件运算符。
      • 强调它只是语法糖,生成 IL 与传统 throw 语句相同。
      • 在 Unity 中,可用于自定义的 NotNull 检查工具方法、表达式体属性中的参数校验等,减少样板代码。
      • 注意不能滥用:避免将复杂逻辑挤进单一表达式,影响调试与可读性。

14.5 default 字面量(C# 7.1)

  • 核心概念:C# 7.1 允许在编译器能推断目标类型的上下文中,直接使用 default 替代 default(T),简化代码,尤其对长类型名的可选参数效果显著。

  • 关键点

    • 语法:将 default(T) 简写为 default,由编译器根据目标类型自动推断。
    • 主要应用场景
      • 可选参数默认值CancellationToken cancellationToken = default 替代冗长的 default(CancellationToken)
      • 条件表达式return valid ? value : default;
      • 数组初始化器new[] { default, 5 } 推断为 int[]new[] { default, "text" } 推断为 string[]
    • 行为特征
      • default 字面量本身没有类型,类似 null 字面量,可隐式转换为任何类型。
      • 必须能从上下文推断目标类型,否则编译错误(如 var x = default; 非法)。
      • 若目标类型是引用类型或原生类型,default 被视为常量表达式,可用于 attribute
    • 潜在混淆
      • default 字面量始终表示类型的默认值(如 int0),而非方法可选参数声明的默认值。
      • 示例:void PrintValue(int value = 10) 调用 PrintValue(default) 输出 0,而非 10
      • 建议避免在可能引起歧义的地方使用 default,以明确意图。
  • 代码示例

    // 可选参数简化
    public async Task<string> FetchValueAsync(
        string key, CancellationToken cancellationToken = default)
    
    // 条件返回值
    static T GetValueOrDefault<T>(IList<T> list, int index) =>
        index >= 0 && index < list.Count ? list[index] : default;
    
    // 数组类型推断
    var intArray = new[] { default, 5 };        // int[]
    var stringArray = new[] { default, "text" }; // string[]
    
    // 注意:default 指的是类型默认值,非参数默认值
    void PrintValue(int value = 10) => Console.WriteLine(value);
    PrintValue(default); // 打印 0,不是 10
    
  • 面试准备建议

    • 重要性:⭐⭐(常用简洁写法,提升代码优雅度)
    • 面试回答要点
      • default 字面量是 C# 7.1 引入的语法糖,主要用于消除 default(T) 的冗余,尤其当类型名很长时。
      • 知道其类型推断规则:必须有足够上下文推断类型,var x = default; 非法。
      • 能指出与可选参数默认值搭配时的语义陷阱,面试时体现对细节的关注。
      • 在 Unity 中,常用于泛型工具方法返回默认值,或简化 CancellationToken 等长类型参数的可选参数声明,使异步操作代码更整洁。

📦14.6 非尾部命名实参

  • 核心概念:C# 7.2 允许命名实参出现在位置实参之前或之间,只要命名实参的位置与其对应的形参位置一致,从而避免歧义并提升代码可读性。

  • 关键点

    • 解决的问题:当方法参数中有字面量(如 nulltrue、数值)时,其含义不易理解。C# 7.2 之前,必须把后续参数全部改为命名实参或引入额外变量,现在可直接对关键参数使用命名实参。
    • 合法调用规则
      • 尾部命名实参M(5, z: 15, y: 10),命名实参在所有位置实参之后,可打乱顺序。
      • 非尾部命名实参M(5, y: 10, 15),命名实参 y:10 的位置与形参 y 的自然位置一致,未引起错位,合法。
      • 非法调用M(y: 10, 5, 15),第一个实参是命名实参但对应 y,后续位置实参无法明确对应 x 还是 z,编译器禁止。
    • 设计原则:非尾部命名实参必须处于“如果它是位置实参也会出现的同一个位置”,杜绝任何可能的参数对应歧义。
  • 代码示例

    void M(int x, int y, int z) { }
    
    // 合法调用
    M(5, z: 15, y: 10);    // 尾部命名实参,顺序可任意
    M(5, y: 10, 15);       // 非尾部命名实参,y:10 在 y 的自然位置
    
    // 非法调用
    // M(y: 10, 5, 15);    // 错误:命名实参打乱了位置对应关系
    
    // 提升可读性的典型应用
    client.UploadCsv(table, schema: null, csvData, options); // 明确哪个参数为 null
    
  • 面试准备建议

    • 重要性:⭐(了解即可,提升代码可读性的细节)
    • 面试回答要点
      • C# 7.2 非尾部命名实参允许在不改变参数顺序的前提下,对中间某个字面量参数添加名称,提升可读性。
      • 记住基本原则:命名实参必须出现在它作为位置实参的“自然”位置。
      • 在 Unity 中,当调用带有大量可选参数或字面量的 API 时(如物理、动画系统的方法),可用此特性明确意图,例如 Physics.Raycast(origin, direction, maxDistance: 100f, layerMask: myMask)

📦14.7 私有受保护的访问权限(C# 7.2)

此部分重要程度较低,如果需要就问问AI吧,就不涉及了。


14.8 C# 7.3 的一些小改进

14.8.1 泛型类型约束(C# 7.3 新增 EnumDelegateunmanaged

  • 核心概念:C# 7.3 允许泛型类型参数显式约束为 Enum(枚举)、Delegate(委托)或 unmanaged(非托管类型),使泛型方法/类型能够对更具体的类别进行操作,增强类型安全性和表达能力。

  • 关键点

    • Enum 约束
      • 通常与 struct 组合:where T : struct, Enum
      • 限制 T 为具体的枚举值类型,排除 System.Enum 本身。
      • 常用场景:编写可处理任意枚举类型的工具方法(如解析、转换、位运算)。
    • Delegate 约束
      • 可直接使用:where T : Delegate
      • 允许 System.DelegateMulticastDelegate 及任何用 delegate 关键字声明的具体委托类型。
      • 无法进一步约束“必须是具体委托类型”(类似枚举约束中的 struct)。
    • unmanaged 约束
      • 限制 T 为非托管类型:不可为引用类型、不可包含托管字段的值类型(如 intfloatGuid、自定义纯值类型结构体)。
      • 允许编写更高效、可直接操作内存的泛型代码(如与指针、Span<T> 交互)。
    • 使用限制
      • 约束为 Enum 时,System.Enum 自身不满足(因不满足 struct 约束)。
      • Delegate 约束可接受 DelegateMulticastDelegate 等抽象类型。
      • unmanaged 约束不能用于 string 等引用类型或包含引用字段的结构体。
  • 代码示例

    // Enum 约束
    static void EnumMethod<T>() where T : struct, Enum { }
    EnumMethod<DayOfWeek>();               // 合法
    // EnumMethod<Enum>();                 // 非法:不满足 struct
    
    // Delegate 约束
    static void DelegateMethod<T>() where T : Delegate { }
    DelegateMethod<Action>();              // 合法
    DelegateMethod<Delegate>();            // 合法
    DelegateMethod<MulticastDelegate>();   // 合法
    
    // unmanaged 约束
    static void UnmanagedMethod<T>() where T : unmanaged { }
    UnmanagedMethod<int>();               // 合法
    UnmanagedMethod<Guid>();              // 合法(Guid 是值类型且字段均为非托管)
    // UnmanagedMethod<string>();         // 非法:string 是引用类型
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,提升类型安全的实用特性)
    • 面试回答要点
      • C# 7.3 新增了 EnumDelegateunmanaged 三种泛型约束,让泛型编程更加精确。
      • Enum 约束通常配合 struct 使用,用于编写枚举工具方法;Delegate 约束用于处理各类委托;unmanaged 约束用于高性能、非托管内存操作场景。
      • 能举例说明在 Unity 中可能的用途:Enum 约束可用于通用枚举解析(如 Editor 工具);unmanaged 约束在编写与原生代码交互或使用 Span<T> 操作时保证类型安全。
      • 注意指出 unmanaged 约束对类型有严格要求,必须所有字段也都是非托管类型,这对自定义结构体的设计有影响。

📦14.8.2 重载决议改进

  • 核心概念:C# 7.3 将部分方法有效性检查(如类型约束、静态/实例调用方式)提前到重载决议过程中,消除了一些原本会导致歧义或编译失败的情况,使重载选择更智能、更符合直觉。

  • 关键点

    • 提前检查的条件
      1. 泛型类型实参必须满足类型形参的所有约束。
      2. 静态方法不能以实例方法的形式调用。
      3. 实例方法不能以静态方法的形式调用。
    • 改进效果
      • 如果某个重载候选因上述条件根本不可能有效,编译器会在重载决议阶段就将其排除,而不是先选出它、再报错。
      • 示例:两个重载分别约束 T : structT : class,调用时传入 int,编译器现在能正确选择 struct 约束的方法,而非报错或产生歧义。
    • 设计目的:使泛型重载更加可用,减少开发者为满足重载决议而添加的额外强制类型转换或临时变量。
  • 代码示例(约束检查提前):

    static void Method<T>(object x) where T : struct =>
        Console.WriteLine($"{typeof(T)} is a struct");
    static void Method<T>(string x) where T : class =>
        Console.WriteLine($"{typeof(T)} is a reference type");
    
    // C# 7.3 前可能报错,C# 7.3 可正确选择第一个方法
    Method<int>("text"); // 输出: System.Int32 is a struct
    
  • 面试准备建议

    • 重要性:⭐(了解即可,展现对编译器演进的理解)
    • 面试回答要点
      • 如果被问到“为什么某个泛型重载在旧版本中报错但在新版本中正常”,可解释说 C# 7.3 改进了重载决议,将约束检查提前,从而排除不可行的候选方法。
      • 在实际编码中较少遇到,但对于设计泛型 API 的开发者来说,这能减少不必要的重载歧义。
      • 提及静态/实例方法调用方式的检查也一同提前,使得误用能更早被发现。

14.8.3 字段的attribute支持自动实现的属性

  • 核心概念:C# 7.3 允许直接对自动实现属性的隐式背后字段应用 attribute,使用 [field: AttributeName] 语法,消除了必须手动声明字段和完整属性的样板代码。

  • 关键点

    • 语法:在自动实现属性前添加 [field: Attribute],该 attribute 会编译到编译器生成的私有字段上。
    • 解决的问题
      • C# 7.3 之前,若想为支持字段添加 attribute(如 [NonSerialized][SerializeField]),必须放弃自动实现属性,手动展开属性及字段。
      • 现在可同时享受自动属性的简洁与字段级 attribute 的控制。
    • field 目标修饰符field: 并不是新关键字,而是 C# 一直存在的 attribute 目标修饰符,只是此前未对自动实现属性开放。Mono 编译器曾非正式支持,C# 7.3 将其标准化。
    • 常见应用
      • [field: NonSerialized] 标记不需序列化的自动属性。
      • [field: SerializeField] 在 Unity 中序列化私有自动属性。
      • 任何需要标记背后字段的框架或库特性。
  • 代码示例(新旧对比):

    // C# 7.3 之前:手动展开
    [Demo]
    private string name;
    public string Name
    {
        get => name;
        set => name = value;
    }
    
    // C# 7.3:直接标记自动属性的字段
    [field: Demo]
    public string Name { get; set; }
    
  • 面试准备建议

    • 重要性:⭐⭐(实用特性,特别是对 Unity 开发者)
    • 面试回答要点
      • 能解释 [field: ...] 语法将 attribute 应用于自动属性的隐藏背后字段。
      • 对比新旧写法,突出减少样板代码的优势。
      • Unity 重点:在 Unity 中,经常需要用 [SerializeField] 序列化私有字段,以前只能手动声明字段;现在可以写 [field: SerializeField] public string Name { get; set; },兼顾封装与序列化。这是面试中展示 Unity 和 C# 结合经验的加分点。
      • 注意:[field: SerializeField] 序列化的是自动属性的背后字段,但属性本身仍通过 getter/setter 访问,Unity 的序列化系统会直接操作字段,绕过了属性的逻辑,使用时需要清楚这一点。
posted @ 2026-07-29 14:55  绘星tsuki  阅读(1)  评论(0)    收藏  举报