[深入解析C#] 第 12 章:分解与模式匹配

📅 2026/7/28 20:08:50 👁️ 阅读次数 📝 编程学习
[深入解析C#] 第 12 章:分解与模式匹配

12.1 分解元组

❗️12.1.1 分解成新变量

  • 核心概念:分解(deconstruction)允许将元组或其他类型的值拆分为多个独立变量,语法简洁,支持声明新变量或赋值给已有变量,并可与隐式类型推断、丢弃符等结合。

  • 关键点

    • 两种分解方式
      • 声明新变量(int a, string b) = tuple;var (a, b) = tuple;(隐式类型简写)。
      • 赋值给已有变量(e, f) = tuple;(变量需先声明)。
    • 语法特性
      • 可混合显式类型和 var(int a, var b) = tuple;(但不能在外层 var(...) 内又显式指定类型)。
      • 支持隐式类型转换:(long a, var b) = (1, "text");
      • 分解时变量和元组元素独立,修改互不影响。
    • 丢弃符 _
      • _ 忽略不需要的元素:var (x, _, _, y) = tuple;
      • _ 不引入新变量,不能后续使用,已有同名变量不受影响。
    • 编译器转换:分解本质上引入临时变量,再将元素逐一赋值给目标变量,避免多次调用方法。
    • 避免混淆(int c, string d) = tuple; 是分解声明,而 (int c, string d) x = tuple; 是声明元组变量,两者语法接近,需注意区分。
  • 代码示例

    // 方法返回元组
    static (int x, int y, string text) GetData() => (1, 2, "hello");// 1. 分解为新变量(显式类型)
    (int a, int b, string name) = GetData();// 2. 全部隐式类型(var 前置)
    var (x, y, s) = GetData();// 3. 混合显式/隐式(允许)
    (long l, var s2) = GetData(); // 截取前两个元素,忽略 text// 4. 赋值给已有变量
    int e; string f;
    (e, f) = (10, "world");// 5. 丢弃不需要的元素
    var (first, _, _, last) = (1, 2, 3, 4);
    
  • 面试准备建议

    • 重要性:⭐⭐⭐(C# 7 核心特性,面试高频)
    • 面试回答要点
      • 分解用于将元组(或自定义类型)拆分为独立变量,增强代码可读性,避免临时变量。
      • 掌握三种形式:声明新变量、赋值现有变量、混合使用 var 和显式类型。
      • 丢弃符 _ 用于忽略不关心的元素,不分配变量,常见于模式匹配和分解中。
      • 能对比分解与元组声明:(int x, int y) = ... 是分解,(int x, int y) v = ... 是元组变量。
      • 在 Unity 中应用:解析方法返回的多值(如坐标、状态),简洁获取部分字段,提高代码流畅度。

12.1.2 通过分解为已有变量/属性赋值及执行细节

  • 核心概念:分解操作不仅可声明新变量,还能为已有的局部变量、字段、属性或索引器赋值,实现一条语句同时更新多个目标,且支持丢弃符 _。但声明与赋值不可混用,赋值顺序有严格规定。

  • 关键点

    • 赋值分解的适用范围
      • 目标可以是局部变量、字段、属性、数组/集合索引器。
      • 不能同时声明新变量和赋值给已有变量(如 (x, int y) = ... 非法)。
      • 各类目标可以混合,如同时赋值给局部变量和属性。
    • 丢弃符行为
      • 当作用域中不存在 _ 变量时,_ 丢弃值。
      • 当存在同名变量时,_ 视为普通赋值目标(正常赋值)。
      • 建议:避免将 _ 用作局部变量名,杜绝歧义。
    • 构造器简写模式
      • 可结合元组字面量和分解,在表达式主体构造器中一次性赋值所有属性:

        public Point(double x, double y) => (X, Y) = (x, y);
        
      • 优势:极简、映射关系清晰。

      • 代价:需要依赖 System.ValueTuple.dll,可能增加程序集引用。

      • 仅建议在项目已使用元组/引用相应库时采用,且不宜用于包含复杂校验的构造器。

    • 赋值分解的执行顺序(三阶段)
      1. 从左到右计算所有赋值目标(如属性需先提取对象引用)。
      2. 计算右侧的元组字面量或表达式。
      3. 从左到右执行实际赋值。
      • 陷阱:若目标和右侧存在依赖关系,可能导致非预期结果,应避免分解中有副作用的复杂表达式。
  • 代码示例

    // 为已有变量赋值
    int a, b; string name;
    (a, b, name) = MethodReturningTuple();// 混合目标赋值(局部变量 + 属性)
    int x;
    (x, someObject.SomeProperty) = (10, "value");// 执行顺序演示(反面案例)
    StringBuilder builder = new StringBuilder("12345");
    (builder, builder.Length) = (new StringBuilder("67890"), 3);
    // builder.Length 赋给了原始 StringBuilder,而非新的
    // 原始 builder 内容变为 "123",新 builder 为 "67890"
    
  • 面试准备建议

    • 重要性:⭐⭐(理解即可,非高频但体现深度)
    • 面试回答要点
      • 分解赋值可同时更新多个变量/属性/字段,比逐条赋值更简洁。
      • 构造器中使用分解赋值是 C# 7 的一种优雅模式,但需权衡依赖和复杂度。
      • 理解赋值的执行顺序(目标、右侧、赋值),避免写出依赖求值顺序的脆弱代码。
      • 丢弃符 _ 仅在无同名变量时丢弃值,建议在解构中始终将其视为“不关心”,避免与已有变量冲突。
      • 在 Unity 中,可用于从方法返回的配置元组中快速更新多个组件属性,如 (player.Health, player.Score) = CalculateNewStats();

📦12.1.3 元组字面量分解的细节

  • 核心概念:分解操作中的元组字面量即使本身无类型(如包含 null 或 lambda),只要每个元素能转换为目标类型,分解即可成立。隐式转换同样适用于分解。

  • 关键点

    • 无类型元组字面量的分解
      • 元组字面量 (null, x => x * 2) 无类型,但可分解到 (string text, Func<int, int> func),因为元素级存在转换。
      • 声明分解和赋值分解都适用此规则。
    • 隐式转换支持
      • 常量可隐式转换为兼容类型:(byte x, byte y) = (5, 10); 合法,因 510byte 范围内。
    • 设计一致性:分解的类型兼容规则与元组字面量到元组类型的转换规则一致,都是逐元素检查转换可行性。
  • 代码示例

    // 无类型元组字面量分解(声明)
    (string text, Func<int, int> func) = (null, x => x * 2);// 无类型元组字面量分解(赋值)
    string text; Func<int, int> func;
    (text, func) = ("text", x => x * 3);// 隐式转换分解
    (byte x, byte y) = (5, 10);
    
  • 面试准备建议

    • 重要性:⭐(了解即可,非核心)
    • 面试回答要点
      • 分解操作的类型兼容是按元素进行的,即使整体元组字面量无类型,只要逐元素可隐式转换就行。
      • 行为与元组字面量赋值/转换一致,保持语言规则统一。
      • 实际应用中不需要特别记忆,遇到相关编译错误时能理解原因即可。

12.2 非元组类型的分解操作

12.2.1 实例分解方法

  • 核心概念:任何类型只要提供了合适的 Deconstruct 方法(实例方法或扩展方法),就可以像元组一样使用分解语法,将对象拆分为多个独立变量。这是一种基于模式的语言特性,与 async/awaitforeach 的模式类似。

  • 关键点

    • Deconstruct 方法要求

      • 必须是 void 返回类型。
      • 所有参数必须使用 out 修饰符。
      • 参数个数至少 2 个(因为分解至少两个值才有意义)。
      • 必须是非泛型方法。
      • 对于调用代码必须是可访问的(如 public 或同程序集的 internal)。
    • 为什么用 out 参数而非返回元组

      • 允许一个类型有多个不同参数个数的 Deconstruct 重载(重载不能仅靠返回值区分)。
      • 例如 DateTime 可以有 Deconstruct(out int year, out int month, out int day)Deconstruct(out int year, out int dayOfYear) 两种分解方式。
    • 实例方法与构造器的对称性

      • 构造器和分解器可以形成优雅的同构:

        public Point(double x, double y) => (X, Y) = (x, y);       // 构造
        public void Deconstruct(out double x, out double y) => (x, y) = (X, Y); // 分解
        
    • 扩展方法支持

      • 对于无法修改源码的类型(如 DateTime),可通过扩展方法添加 Deconstruct,使其支持分解语法。
  • 代码示例

    public sealed class Point
    {public double X { get; }public double Y { get; }public Point(double x, double y) => (X, Y) = (x, y);// 实例分解方法public void Deconstruct(out double x, out double y){x = X;y = Y;}
    }// 使用分解
    var point = new Point(1.5, 20);
    var (x, y) = point;  // x = 1.5, y = 20// 用扩展方法为 DateTime 添加分解(示例)
    public static class DateTimeExtensions
    {public static void Deconstruct(this DateTime dt, out int year, out int month, out int day)=> (year, month, day) = (dt.Year, dt.Month, dt.Day);
    }
    // 使用:var (y, m, d) = DateTime.Now;
    
  • 面试准备建议

    • 重要性:⭐⭐(C# 7 重要特性,体现对语言模式的理解)
    • 面试回答要点
      • 分解操作不仅限于元组,任何类型通过提供 Deconstruct(out ...) 方法即可被分解,包括扩展方法。
      • Deconstruct 方法必须返回 void、参数全为 out、至少两个参数、非泛型。
      • 使用 out 参数的原因:支持重载(一个类型可多种分解方式),而重载不能仅靠返回值区分。
      • 构造器与分解器可形成对称关系,代码简洁且语义清晰。
      • 在 Unity 中,可为 Vector3Color 等已有关键类型编写扩展分解方法,方便将值拆分到多个变量,提升代码可读性。

12.2.2 扩展分解方法与重载

  • 核心概念:无法修改源码的类型可以通过扩展方法添加 Deconstruct,并且一个类型可以有多个不同参数个数的 Deconstruct 重载,编译器根据分解时所需的变量数量自动选择合适的方法。

  • 关键点

    • 扩展 Deconstruct 方法
      • 第一个参数是 this 目标类型,其余参数必须全是 out
      • 可见性需符合调用位置要求(如 publicinternal)。
      • 泛型限制:方法本身可以是泛型的,但只有第一个参数能参与类型推断,其余 out 参数不做类型推断。
    • 重载解析
      • 同一类型可定义多个 Deconstruct 重载,参数个数必须不同(至少2个)。
      • 编译器根据分解赋值左侧变量数量匹配最合适的重载。
      • 实例方法和扩展方法可以共存,若实例方法不匹配(参数个数不对),编译器会继续查找合适的扩展方法。
    • 使用场景
      • 为 BCL 类型(如 DateTimeDateTimeOffset)、第三方库类型或 Unity 内置类型添加便利分解。
      • 在不同上下文中根据需要拆分不同粒度的数据。
  • 代码示例

    // 扩展方法:将 DateTime 分解为年月日
    static void Deconstruct(this DateTime dt, out int year, out int month, out int day) =>(year, month, day) = (dt.Year, dt.Month, dt.Day);// 重载:分解为年月日时分秒
    static void Deconstruct(this DateTime dt,out int year, out int month, out int day,out int hour, out int minute, out int second) =>(year, month, day, hour, minute, second) =(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);// 使用
    var birthday = new DateTime(1976, 6, 19);
    var now = DateTime.UtcNow;var (y, mo, d, h, mi, s) = now; // 调用 6 参数重载
    (y, mo, d) = birthday;          // 调用 3 参数重载
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现对模式扩展和 API 设计的理解)
    • 面试回答要点
      • 分解功能通过模式实现,不只局限于元组,可通过扩展方法让任何类型支持分解。
      • 重载 Deconstruct 能提供多种分解粒度,方法签名靠 out 参数数量区分。
      • 扩展分解方法的泛型限制:只有目标参数参与类型推断,out 参数不能。
      • 可举例:为 Unity 的 Vector3Color 等类型编写扩展分解方法,使代码更简洁,如 var (r, g, b) = myColor;
      • 强调这是一种无侵入式的类型增强,符合开闭原则。

📦12.2.3 编译器对 Deconstruct 调用的处理

  • 核心概念:编译器将非元组类型的分解转换为对 Deconstruct 方法的调用,通过 out var 临时变量接收分解值,再赋值给声明的变量。分解目标变量类型不参与 Deconstruct 方法的重载决议和类型推断。

  • 关键点

    • 转换过程

      (int x, string y) = target;
      // 编译器转换为:
      target.Deconstruct(out var tmpX, out var tmpY);
      int x = tmpX;
      string y = tmpY;
      
      • 先调用 Deconstruct,用隐式类型 out var 临时变量接收结果。
      • 再将临时变量赋值给声明的目标变量(可能发生隐式类型转换)。
    • 关键影响——目标变量类型不参与推断

      • Deconstruct 实例方法不能是泛型方法,因为源码中的变量类型(如 int x)不参与类型推断,泛型无法推断类型实参。
      • Deconstruct 扩展方法可以是泛型方法,因为第一个参数(this 目标类型)参与类型推断,但 out 参数不能。
      • 重载解析只看 out 参数个数:编译器仅根据分解时变量数量匹配参数个数合适的 Deconstruct,不关心具体类型。若存在多个同参数个数的重载,编译将失败。
    • 执行时序:与元组分解相同,遵循目标计算 → 右侧计算 → 赋值三阶段。

    • 调试技巧:遇到分解相关编译错误时,手动将分解代码转换为 target.Deconstruct(out ...) 形式,有助于理解问题根源。

  • 代码示例(编译器视角):

    // 假设 target 是某种自定义类型,具有 Deconstruct 方法
    (int x, string y) = target;// 编译器的等价处理(伪代码)
    target.Deconstruct(out var tmpX, out var tmpY);
    int x = tmpX;
    string y = tmpY;
    
  • 面试准备建议

    • 重要性:⭐(了解编译原理,非直接考点)
    • 面试回答要点
      • 分解操作的变量类型不参与 Deconstruct 方法的选择和类型推断,只有 out 参数数量决定调用哪个重载。
      • 这就是为什么 Deconstruct 实例方法不能是泛型——没有足够信息推断类型形参。
      • 扩展 Deconstruct 方法可以是泛型,因为目标参数(this)参与推断。
      • 若类型有两个参数个数相同但类型不同的 Deconstruct 重载,编译器因歧义报错。
      • 理解该机制有助于解释分解操作相关的编译错误,在面试中能展示对 C# 语言设计的深入理解。

❗️12.3 模式匹配简介

  • 核心概念:C# 7 引入的模式匹配允许检查某个值的特定特征(类型、值等),并根据检查结果执行相应操作,同时为后续代码提供强类型的上下文信息。这是 C# 向函数式语言特性靠拢的重要一步。

  • 关键点

    • 解决的问题
      • 消除传统 as + != null 类型检查模式的冗余和重复。
      • 使条件逻辑更清晰,意图更明确,减少出错可能。
    • 新语法能力
      • switch 语句的 case 标签不仅支持常量值,还支持类型模式(如 case Rectangle rect:)和 null 模式case null:)。
      • 类型模式在匹配成功的同时会声明并初始化一个强类型变量,作用域仅限于该 case 块。
      • 如果匹配的值是 nullcase null: 可以专门处理,避免空引用异常。
    • 两大部分
      1. 模式本身:C# 7.0 引入的几种具体模式(常量模式、类型模式、var 模式)。
      2. 模式应用的上下文:可以在哪些语法结构中使用模式(is 表达式、switch 语句等)。
    • 设计前瞻:C# 设计团队精心设计了模式语法,为未来添加更多模式(如递归模式、属性模式等)预留了空间,旨在逐步增强语言的表达能力。
  • 代码示例(对比新旧写法):

    // C# 6 及之前:冗长的类型检查
    static double Perimeter(Shape shape)
    {if (shape == null)throw new ArgumentNullException(nameof(shape));Rectangle rect = shape as Rectangle;if (rect != null)return 2 * (rect.Height + rect.Width);Circle circle = shape as Circle;if (circle != null)return 2 * Math.PI * circle.Radius;// ... 更多类型throw new ArgumentException(...);
    }// C# 7 模式匹配:清晰简洁
    static double Perimeter(Shape shape)
    {switch (shape){case null:throw new ArgumentNullException(nameof(shape));case Rectangle rect:return 2 * (rect.Height + rect.Width);case Circle circle:return 2 * Math.PI * circle.Radius;case Triangle tri:return tri.SideA + tri.SideB + tri.SideC;default:throw new ArgumentException(...);}
    }
    
  • 面试准备建议

    • 重要性:⭐⭐⭐(C# 7 核心特性,面试高频考点)
    • 面试回答要点
      • 模式匹配解决了传统类型检查的冗余问题,让代码更简洁、意图更清晰。
      • 能够解释 switchcase nullcase Type variable 的新用法,并与旧式 as/is 对比。
      • 知道模式匹配分为“模式种类”和“应用上下文”两部分,是语言功能扩展的基础。
      • 在 Unity 中应用:处理不同类型的游戏事件、不同类型的资源对象、不同 UI 控件的统一回调等,使代码更易维护。
      • 回答时能指出 C# 7 的模式只是起点,后续版本会增强(如 C# 8+ 的属性模式、位置模式),体现对技术演进的关注。

12.4 C# 7.0的可用模式

📦12.4.1 常量模式

  • 核心概念:常量模式使用编译时常量对 input 进行等价检查。若双方为整型则使用 == 比较;否则使用静态方法 object.Equals(input, 常量) 以保证空值安全。

  • 关键点

    • 模式定义:由任何编译时常量表达式组成。
    • 比较规则
      • input 和常量都是整型表达式 → 使用 == 比较(适用于 is 左侧为整型变量的场景)。
      • 其他类型 → 使用 object.Equals(input, 常量),该静态方法可安全处理 null 值。
    • 装箱陷阱:当 inputobject 时,常量 10int,而传入的 10L 是装箱的 long (Int64),object.Equals(10L, 10) 返回 false。这是初学者容易踩的坑。
    • 用途:常用于 switch 中处理特定常量值(如 case null:),或与 is 结合进行快速值检查,但类型模式更常用。
  • 代码示例

    void Match(object input)
    {if (input is "hello")          // 字符串常量,object.EqualsConsole.WriteLine("hello");else if (input is 5L)           // long 常量,object.EqualsConsole.WriteLine("5L");else if (input is 10)           // int 常量,object.EqualsConsole.WriteLine("10");
    }Match(10L); // 输出 false 分支!因为 10L (装箱 long) ≠ 10 (装箱 int)// 整型变量使用 == 比较
    long x = 10L;
    if (x is 10)  // true,因为左侧是 long 变量,执行 == 比较Console.WriteLine("x is 10");
    
  • 面试准备建议

    • 重要性:⭐(理解即可,非高频)
    • 面试回答要点
      • 常量模式用于值与常量的精确匹配,语法 input is 常量
      • 区分整型 == 比较和 object.Equals 比较,能解释装箱情况下可能出现的“意料之外的不匹配”。
      • 面试时如果被问到模式匹配的分类,能说出常量模式是三种基本模式之一即可。大部分实际应用中类型模式更突出,常量模式多用于 null 检查等特殊场景。

❗️12.4.2 类型模式

  • 核心概念:类型模式将类型检查与变量声明结合,匹配成功时引入指定类型的变量,直接持有转换后的值,null 不会匹配任何类型。

  • 关键点

    • 语法input is Type variable,如果 inputType 的实例(且非 null),则匹配成功,并声明变量 variable 赋值为 input 转换后的值。
    • as/if 对比:一行代码替代类型检查、声明、类型转换三步,更简洁且类型安全。
    • null 行为null 不匹配任何类型,因此模式变量一定非 null。
    • 丢弃符_ 可以作为变量名,不引入新变量,仅作匹配(如 shape is Circle _)。
    • 可空值类型限制:类型模式中不能直接使用可空值类型(如 int? t),但可以使用类型形参,当类型实参为可空值类型时,只有非空值才能匹配成功。
    • C# 7.0 的泛型限制:要求输入表达式的编译时类型能显式转换为目标类型。泛型方法中 T 不能直接转换为具体类型,导致类型模式编译错误。C# 7.1 已修复,使类型模式采用 as 语义,兼容任何类型。
    • 优先级:类型模式是三种基本模式中最常用的。
  • 代码示例

    // 替换 as/if 的典型用法
    if (shape is Rectangle rect)return 2 * (rect.Height + rect.Width);// switch 中使用类型模式
    switch (shape)
    {case Circle c:Console.WriteLine($"Radius: {c.Radius}");break;case Rectangle r:Console.WriteLine($"{r.Width} x {r.Height}");break;// ...
    }// 可空值类型匹配(通过泛型)
    void CheckType<T>(object value)
    {if (value is T t)Console.WriteLine($"Yes! {t} is a {typeof(T)}");elseConsole.WriteLine($"No! {value ?? "null"} is not a {typeof(T)}");
    }
    CheckType<int?>(null); // No, null not match
    CheckType<int?>(5);    // Yes, 5 is a Nullable<int32>
    
  • 面试准备建议

    • 重要性:⭐⭐⭐(核心模式,面试高频)
    • 面试回答要点
      • 类型模式是 C# 7 模式匹配的基础,将类型检查和变量声明合二为一,消除冗余代码,避免额外的类型转换。
      • null 永远不会匹配类型模式,因此引入的模式变量非空,能简化空值处理。
      • switch 语句中,类型模式让多类型分支判断变得清晰、可读,能替代大段 if-else + as
      • 提到 C# 7.1 的改进,能体现对语言版本差异的了解。
      • 在 Unity 中适用场景:处理不同碰撞体类型(ColliderBoxCollider)、不同 UnityEvent 参数、自定义节点系统等,方便根据对象实际类型执行不同逻辑。

📦12.4.3 var模式

  • 核心概念var 模式是一种特殊的模式,它总是匹配成功,并引入一个与输入表达式类型和值完全相同的新变量,即使输入为 null 也不例外。

  • 关键点

    • 语法expression is var variable
    • 行为特点
      • 不进行任何类型检查或值检查,匹配总是成功。
      • 引入的变量类型就是输入表达式的编译时类型,值就是输入的值。
      • 输入为 null 时同样匹配,变量值为 null(这是它与类型模式的最大区别)。
    • 主要用途
      • switch 中作为“兜底”分支:当所有特定模式都未匹配时,使用 case var captured: 捕获原始值,以便在错误消息或日志中输出。
      • 配合哨兵语句(when:在需要基于变量进行计算但不关心类型时使用(12.6.1 节将详述)。
    • 与常量模式/类型模式的对比
      • 常量模式:检查值与常量相等。
      • 类型模式:检查值是否为某类型(且非 null)。
      • var 模式:不做任何过滤,纯粹引入变量。
    • 设计意图:为模式匹配提供一个“万能捕获”并给出命名变量的机制,避免在 switch 外部预先声明变量或使用 default 时无法获取值。
  • 代码示例

    static double Perimeter(Shape shape)
    {switch (shape ?? CreateRandomShape()){case Rectangle rect:return 2 * (rect.Height + rect.Width);case Circle circle:return 2 * Math.PI * circle.Radius;case Triangle tri:return tri.SideA + tri.SideB + tri.SideC;// var 模式:捕获所有未匹配的情况,包括 nullcase var actualShape:throw new InvalidOperationException($"Unknown shape type: {actualShape?.GetType()}");}
    }
    
  • 面试准备建议

    • 重要性:⭐(了解即可,非核心)
    • 面试回答要点
      • var 模式总是成功,用于在模式匹配中引入一个与输入同类型的变量,不会过滤任何值。
      • 它在 switch 中最常充当“捕获所有”的分支,可以获得原始值以便记录或抛出异常。
      • default 的区别:default 无法提供具名变量,而 var 模式可以。
      • 知道 var 模式与类型模式、常量模式一起构成 C# 7.0 的三种基本模式即可。

12.5 模式匹配与 is 运算符的搭配使用(模式变量的作用域和确定赋值)

  • 核心概念is 运算符现在可接模式,类型模式是最常见用法。引入的模式变量作用域为闭合块,但其“确定赋值”状态取决于上下文,编译器会据此允许或阻止访问。

  • 关键点

    • is 运算符扩展is 右端可以是常量模式、类型模式或 var 模式,类型模式最常用。
    • 模式变量的作用域:声明在 is 表达式中的变量,其作用域为整个闭合块(如 if 语句、方法体)。即使条件不匹配,变量依然在作用域中,只是未被确定赋值。
    • 确定赋值规则
      • if (input is Type variable) 之后,变量在 true 分支中被认为已确定赋值。
      • 在复合条件中(如 input is int x && x > 100),只有第一个条件为 true 并进入右侧时,x 才被确定赋值,因此可在 && 右侧和 if 块内安全使用。
      • || 运算符两侧的模式变量可能只有一个被确定赋值,导致另一个在后续代码中不可直接使用。
    • C# 7.3 改进:移除了模式变量不能用于字段初始化器、属性初始化器、构造器初始化器和查询表达式的限制。
    • 实际价值:允许在一个条件链中复用模式变量,减少重复的类型转换,使代码更流畅。
  • 代码示例

    // 典型用法:类型模式 + is
    if (input is string text)
    {Console.WriteLine($"String: {text}"); // text 确定赋值
    }
    else if (input is StringBuilder builder)
    {text = builder.ToString(); // text 在 else if 中未确定赋值,但可以赋值给它
    }
    // 此处 text 在作用域内,但可能未被赋值,编译器会检查// 组合条件:利用确定赋值
    if (input is int x && x > 100)
    {Console.WriteLine($"Large int: {x}"); // x 确定赋值
    }// 多类型检测(注意变量作用域)
    if ((input is int a && a > 100) || (input is long b && b > 100))
    {// a 和 b 都在作用域内,但只有被匹配的那个才确定赋值
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现代码精确控制能力)
    • 面试回答要点
      • is 运算符可与模式联用,模式变量作用域为闭合块,但编译器通过“确定赋值”规则保证使用安全。
      • 能解释为什么 if (obj is string s && s.Length > 5) 合法,而单独在条件外使用 s 可能报错。
      • C# 7.3 之前模式变量不能在初始化器中使用,现已放开。
      • 在 Unity 中,可用于处理不同类型的组件或数据,通过 is 安全地获取强类型变量并立即使用,同时避免不必要的 as 转换和空值检查。

12.6 在 switch 语句中使用模式

12.6.1 哨兵语句

  • 核心概念:C# 7 的 switch 不再限于常量匹配,case 标签可包含模式并可附加 when 哨兵语句,实现了更灵活、声明式的多分支逻辑。

  • 关键点

    • 模式化的 switch
      • case 可使用所有模式:常量模式、类型模式、var 模式等。
      • 基于常量的 switch 实质上也是模式匹配的特例,行为一致。
    • 哨兵语句(when
      • 语法:case pattern when expression:
      • 仅当模式匹配成功哨兵表达式为 true 时才执行该分支。
      • 同一模式可因 when 条件不同而出现多次,处理更细粒度的逻辑。
    • 模式变量的作用域
      • case 中引入的模式变量仅在该 case 块内有效
      • 不同 case 可重用相同的变量名(如 case GenericParameter gp)。
    • 设计优势
      • if/else 链更声明式,直接表达“输入-输出”关系,适合数学定义、业务规则等。
      • default 结合可保证穷举,提升代码安全性。
  • 代码示例

    // 斐波那契数列(递归定义直译)
    static int Fib(int n)
    {switch (n){case 0: return 0;case 1: return 1;case var _ when n > 1: return Fib(n - 2) + Fib(n - 1);default: throw new ArgumentOutOfRangeException(nameof(n));}
    }// 哨兵语句细化相同模式(Noda Time 示例简化)
    private string GetUid(TypeReference type, bool useTypeArgumentNames)
    {switch (type){case GenericParameter gp when useTypeArgumentNames:return gp.Name;case GenericParameter gp when gp.DeclaringType != null:return $"{gp.Position}";case GenericParameter gp when gp.DeclaringMethod != null:return $"{gp.Position}";case GenericParameter gp:throw new InvalidOperationException("Unhandled generic parameter");case GenericInstanceType git:return "(complex formatting)";default:return type.FullName.Replace('/', '.');}
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现现代 C# 分支处理能力)
    • 面试回答要点
      • switch 模式匹配让多分支逻辑更清晰,支持类型检查、值检查并直接绑定变量。
      • when 哨兵语句在模式之上附加条件,避免冗长的 if 嵌套,同一个模式可因条件不同而出现多次。
      • 注意模式变量作用域仅限于所在 case,不同 case 可重用变量名,不会污染外部。
      • 在 Unity 中应用场景:
        • 状态机:根据当前状态枚举和附加条件(如计时器)执行不同行为。
        • 处理不同类型的游戏事件:case DamageEvent dmg when dmg.Amount > 0: 处理有效伤害。
        • 根据 GetComponent 结果类型进行不同的初始化逻辑。
      • 能对比 switch 模式匹配与 if/else 链,强调声明式风格和可读性提升。

📦12.6.2 case 标签中模式变量的作用域

  • 核心概念:在 case 标签中由模式引入的变量、when 哨兵语句中的模式变量以及 out 变量的作用域仅限于该 case;若多个 case 标签共享同一个 case 体,这些标签中的模式变量不能重名,并且在共享的 case 体中它们不是确定赋值的。

  • 关键点

    • 作用域限制
      • 模式变量仅在对应的 case 体内部有效,不会污染其他 case 或外部作用域。
      • 不同 case 标签可定义同名模式变量,互不冲突(针对独立 case 体)。
    • 共享 case 体的限制
      • 多个 case 标签共享同一个 case 体时,各标签引入的模式变量不能重名。
      • 在共享的 case 体中,这些模式变量都不是确定赋值的,因为编译器不知道实际匹配了哪个标签。
      • 模式变量通常只在 when 子句中被使用(此时已确定匹配,变量已确定赋值),共享的 case 体中很少使用它们。
    • 与普通局部变量区别:普通的 case 体内声明的局部变量作用域是整个 switch 语句(包括其他 case),而模式变量作用域被限定为当前 case 体。
    • 顺序重要性:基于模式的 switch 中,case 标签的顺序会影响执行结果(与之前基于常量的 switch 不同,因为多个模式可能重叠)。
  • 代码示例

    static void CheckBounds(object input)
    {switch (input){// 两个标签共享一个 case 体,模式变量名不同case int x when x > 1000:case long y when y > 10000L:// x 和 y 在此都不是确定赋值,不能直接使用Console.WriteLine("Value is too large");break;case int x when x < -1000:case long y when y < -10000L:Console.WriteLine("Value is too low");break;default:Console.WriteLine("Value is in range");break;}
    }
    
  • 面试准备建议

    • 重要性:⭐(了解即可,避免编写出有问题的共享 case 代码)
    • 面试回答要点
      • 模式匹配 switch 中的模式变量仅在其匹配的 case 体内有效;独立 case 可重用同名变量。
      • 当多个标签共享一个 case 体时,这些标签的模式变量不能重名,且在该 case 体中它们都不是确定赋值的,不能安全使用。
      • 该机制防止了变量污染外部作用域,并让相同模式的重用变得安全。
      • 可提及基于模式的 switch 现在对 case 顺序敏感,因为模式的匹配是自上而下的,可能发生多个模式重叠。

12.6.3 基于模式的 switch 语句的运算顺序

  • 核心概念:基于模式的 switch 严格按源码顺序自上而下匹配 case,一旦匹配成功便不再继续。编译器会检测“屏蔽”(前面的 case 永远拦截后面的相同模式 case)并报错,确保逻辑清晰。

  • 关键点

    • 顺序敏感性
      • 基于模式的 switch 中,case 标签按书写顺序依次尝试匹配。
      • 一旦某个 case 的模式匹配成功且其哨兵(when)条件为 true,就执行该分支并跳出 switch
      • 因此,case 的顺序会影响最终执行结果,这与基于常量的 switch(标签互斥,顺序无关)截然不同。
    • 屏蔽检测
      • 若一个无哨兵的 case(如 case GenericParameter gp:)出现在相同模式的另一个 case 之前,后面的 case 将永远无法匹配。
      • 编译器会识别此类情况并报告错误,阻止编译。
    • default 标签
      • 仅在所有其他 case 匹配失败时执行。
      • 其物理位置不影响行为,但作者强烈建议将其始终放在 switch 末尾以保持可读性。
    • goto 限制:基于模式的 switch 仍然支持 goto case,但只能跳转到常量值标签,且该标签不能有哨兵,实际很少使用。
    • 编译优化:编译器可能对模式匹配的 switch 进行优化(如对同一个类型模式只计算一次类型检查,然后依次测试哨兵),因此生成的 IL 代码可能和源码结构差异较大。
  • 代码示例(必须注意顺序的场景):

    switch (obj)
    {// 特定情况必须在前,否则会被泛型 case 屏蔽case GenericParameter gp when useTypeArgumentNames:return gp.Name;case GenericParameter gp when gp.DeclaringType != null:return $"{gp.Position}";case GenericParameter gp when gp.DeclaringMethod != null:return $"{gp.Position}";// 兜底的泛型 case 必须放在最后,否则编译器会报“屏蔽”错误case GenericParameter gp:throw new InvalidOperationException("Unhandled generic parameter");// ...
    }
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,理解执行模型有助于写出正确分支逻辑)
    • 面试回答要点
      • 基于模式的 switch 是顺序敏感的,更具体的条件或特殊的哨兵必须放在前面,通用的或兜底的放在后面。
      • 编译器通过检测“屏蔽”帮助避免逻辑错误。
      • 对比传统常量 switch 的无序性,强调这是模式匹配带来的新语义。
      • 在 Unity 中应用:处理事件系统时,如果先匹配通用事件类型,特殊子类型可能被忽略;正确排列 case 顺序可确保优先处理特定事件。
      • 能清晰解释顺序规则和屏蔽检测,体现对语言新特性的深入理解。

12.7 对模式特性使用的思考

12.7.1 分解的使用时机

  • 核心概念:模式匹配和分解特性应优先用于提升可读性,而非无原则滥用。分解特别适合元组返回值以及具有自然顺序的数据(坐标、颜色、日期等),不宜为没有明确顺序的业务实体添加公共分解方法。

  • 关键点

    • 模式匹配的使用原则
      • 模式匹配是实现细节,即使使用较多也不会造成严重破坏,但若没有明显可读性提升,可恢复旧式写法。
    • 分解的使用原则
      • 最适合元组返回值:调用方法返回元组后,直接分解为多个变量比保留元组整体更清晰。
      • 适合有自然顺序的类型:如坐标 (x, y, z)、RGB 颜色、日期/时间(年、月、日)等,可考虑提供 Deconstruct 方法。
      • 不适合业务实体:如商品、订单等,其属性没有内在顺序,强行分解只会混淆。
    • 公共 API 中的警告
      • 一旦在公共类型上添加了 Deconstruct 方法,移除它就是破坏性变更,应三思而后行。
      • 大多数类型不应默认实现分解,就像不默认实现 IComparable<T> 一样。
  • 代码示例(元组返回值分解):

    int[] values = { 2, 7, 3, -5, 1, 0, 10 };
    var (min, max) = MinMax(values);   // 直接分解,比保留元组更直观
    Console.WriteLine(min);
    Console.WriteLine(max);
    
  • 面试准备建议

    • 重要性:⭐⭐(理解使用场景,合理应用)
    • 面试回答要点
      • 分解最常见的用法是将方法返回的元组拆成独立变量,减少临时变量,提升可读性。
      • 为类型添加 Deconstruct 方法时,要确保其元素顺序明确且不易产生歧义;避免为纯业务实体提供分解。
      • 强调在公共 API 中谨慎添加 Deconstruct,因为它会成为类型的公开契约的一部分,后期修改是破坏性变更。
      • 模式匹配则相对轻量,可以灵活使用,只要让代码更清晰就可以采用,不清晰时回退到 if/else 也没有问题。

12.7.2 模式匹配的使用时机

  • 核心概念:模式匹配主要用于替代那些检查类型后执行操作或冗长 if/else-if 链的代码,以更声明式的方式表达“匹配条件-执行操作”的意图。

  • 关键点

    • 明确的替换信号
      1. 代码中出现 is / as 运算符配合特定类型值作为条件的模式。
      2. 对同一个值使用大量 if/else-if/else 分支。
        此时可考虑用基于模式的 switch 或带有模式匹配的 is 表达式重构。
    • 警惕过度使用
      • 如果 case var ... when 模式反复出现(逻辑完全在哨兵语句中,模式本身不提供类型筛选),应反思是否真的适合模式匹配,可能传统 if/else 更直白。
      • 即便如此,适度的模式匹配通常可接受,因为其“匹配并执行”的语义比嵌套 if/else 更清晰。
    • 设计层面的重构契机
      • 模式匹配也可能推动更深层的重构,例如将分散在各处的类型判断逻辑集中到一个独立的运算类中,与原有类型解耦。
      • 这种“运算与类型分离”的做法在 C# 中越来越常见,模式匹配为此类设计提供了语法支持。
    • 个人实践建议:多练习、试用,并在编码过程中主动思考哪些旧代码可以受益于模式匹配,逐步形成自己的判断力。
  • 代码示例(模式匹配 vs 传统 if/else):

    // 传统 if/else 链
    static double GetArea(Shape shape)
    {if (shape is Rectangle rect)return rect.Width * rect.Height;else if (shape is Circle circ)return Math.PI * circ.Radius * circ.Radius;else if (shape == null)throw new ArgumentNullException();elsethrow new ArgumentException("Unknown shape");
    }// 使用模式匹配 switch 更声明式
    static double GetArea(Shape shape) =>shape switch{null => throw new ArgumentNullException(),Rectangle rect => rect.Width * rect.Height,Circle circ => Math.PI * circ.Radius * circ.Radius,_ => throw new ArgumentException("Unknown shape")};
    
  • 面试准备建议

    • 重要性:⭐⭐(中等,体现代码品味和重构意识)
    • 面试回答要点
      • 模式匹配适用于类型检查、值范围判断等需要根据输入“形状”执行不同逻辑的场景。
      • 它能将“检查-转换-使用”三步合为一步,减少临时变量和重复代码。
      • 当哨兵条件过于复杂且与类型无关时,不宜强行使用模式匹配,传统条件判断可能更合适。
      • 在 Unity 中,可用来处理不同游戏状态、不同 UI 面板类型、不同网络消息类型的分发,让代码更易扩展和维护。
      • 面试时若被问到代码重构,可以举例说明如何将 if/else 链或类型判断转为模式匹配 switch,并强调其在可读性和维护性上的提升。