[深入解析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,可能增加程序集引用。 -
仅建议在项目已使用元组/引用相应库时采用,且不宜用于包含复杂校验的构造器。
-
- 赋值分解的执行顺序(三阶段):
- 从左到右计算所有赋值目标(如属性需先提取对象引用)。
- 计算右侧的元组字面量或表达式。
- 从左到右执行实际赋值。
- 陷阱:若目标和右侧存在依赖关系,可能导致非预期结果,应避免分解中有副作用的复杂表达式。
- 赋值分解的适用范围:
-
代码示例:
// 为已有变量赋值 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);合法,因5和10在byte范围内。
- 常量可隐式转换为兼容类型:
- 设计一致性:分解的类型兼容规则与元组字面量到元组类型的转换规则一致,都是逐元素检查转换可行性。
- 无类型元组字面量的分解:
-
代码示例:
// 无类型元组字面量分解(声明) (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/await、foreach的模式类似。 -
关键点:
-
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 中,可为
Vector3、Color等已有关键类型编写扩展分解方法,方便将值拆分到多个变量,提升代码可读性。
- 分解操作不仅限于元组,任何类型通过提供
12.2.2 扩展分解方法与重载
-
核心概念:无法修改源码的类型可以通过扩展方法添加
Deconstruct,并且一个类型可以有多个不同参数个数的Deconstruct重载,编译器根据分解时所需的变量数量自动选择合适的方法。 -
关键点:
- 扩展
Deconstruct方法:- 第一个参数是
this目标类型,其余参数必须全是out。 - 可见性需符合调用位置要求(如
public或internal)。 - 泛型限制:方法本身可以是泛型的,但只有第一个参数能参与类型推断,其余
out参数不做类型推断。
- 第一个参数是
- 重载解析:
- 同一类型可定义多个
Deconstruct重载,参数个数必须不同(至少2个)。 - 编译器根据分解赋值左侧变量数量匹配最合适的重载。
- 实例方法和扩展方法可以共存,若实例方法不匹配(参数个数不对),编译器会继续查找合适的扩展方法。
- 同一类型可定义多个
- 使用场景:
- 为 BCL 类型(如
DateTime、DateTimeOffset)、第三方库类型或 Unity 内置类型添加便利分解。 - 在不同上下文中根据需要拆分不同粒度的数据。
- 为 BCL 类型(如
- 扩展
-
代码示例:
// 扩展方法:将 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 的
Vector3、Color等类型编写扩展分解方法,使代码更简洁,如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块。 - 如果匹配的值是
null,case null:可以专门处理,避免空引用异常。
- 两大部分:
- 模式本身:C# 7.0 引入的几种具体模式(常量模式、类型模式、var 模式)。
- 模式应用的上下文:可以在哪些语法结构中使用模式(
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 核心特性,面试高频考点)
- 面试回答要点:
- 模式匹配解决了传统类型检查的冗余问题,让代码更简洁、意图更清晰。
- 能够解释
switch中case null和case 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值。
- 若
- 装箱陷阱:当
input是object时,常量10是int,而传入的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,如果input是Type的实例(且非 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 中适用场景:处理不同碰撞体类型(
Collider、BoxCollider)、不同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链的代码,以更声明式的方式表达“匹配条件-执行操作”的意图。 -
关键点:
- 明确的替换信号:
- 代码中出现
is/as运算符配合特定类型值作为条件的模式。 - 对同一个值使用大量
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,并强调其在可读性和维护性上的提升。