第12章 分解与模式匹配
第12章 分解与模式匹配
12.1 分解元组
分解元组,即将元组的元素分解、复制给其他变量。下面是一个元组分解的示例:
var tuple = (10, "text"); // 创建一个元组类型 (int, string)
var (a, b) = tuple; // 隐式地将元组分解到两个新变量 a 和 b
(int c, string d) = tuple; // 显式地将元组分解到两个新变量 c 和 d
int e; //
string f; // 分解到现有变量
(e, f) = tuple; //
Console.WriteLine($"a: {a}; b: {b}"); //
Console.WriteLine($"c: {c}; d: {d}"); // 证明分解成功
Console.WriteLine($"e: {e}; f: {f}"); //
Notice
比较如下两行代码,哪句是在声明元组示例?哪句是在分解元组?
(int c, string d) = tuple; (int c, string d) x = tuple;第 1 条语句是在分解、第 2 条语句是在声明。编码时请注意这些细节。
12.1.1 分解成新变量
分解这一特性的出现让变量的赋值简化了很多。以如下代码为例:
var tmp = MethodReturningTuple();
int a = tmp.x;
int b = tmp.y;
string name = tmp.text;
static (int x, int y, string text) MethodReturningTuple() => (1, 2, "t");
可以看到它需要借助临时变量完成赋值,且语句繁琐。再看借助分解特性的赋值,只需一行代码便可以完成:
(int a, int b, string name) = MethodReturningTuple();
static (int x, int y, string text) MethodReturningTuple() => (1, 2, "t");
关于变量类型,C# 还支持隐式类型和显式类型混用、全部进行隐式推断:
(int a, int b, var name) = MethodReturningTuple();
(long a, var b, XNamespace name) = MethodReturningTuple();
var (a, b, name) = MethodReturningTuple();
对于不需要的变量,还可以通过 _ 符号进行丢弃:
(int a, _, string name) = MethodReturningTuple();
12.1.2 通过分解操作为已有变量或者属性赋值
利用分解操作,还可以对已有变量/字段/属性/索引器/其他对象进行赋值:
int a = 20;
int b = 30;
string name = "before";
(a, b, name) = MethodReturningTuple();
Error
利用分解操作,可以声明并初始化变量,或者执行一系列赋值,但是不能将二者混用。例如以下代码非法:
int x; (x, int y) = (1, 2);
这种赋值方式也存在一些陷阱。要搞清楚这个陷阱,我们先要知道赋值分解的执行步骤:
- 赋值目标 进行运算;
- 赋值操作右半部分 进行运算;
- 完成赋值。
这种执行顺序可能造成非预期的执行结果。以如下代码为例,它在分解中同时对 StringBuilder、StringBuilder.Length 进行了赋值,输出内容为: 123 、 67890
StringBuilder builder = new StringBuilder("12345");
StringBuilder original = builder;
(builder, builder.Length) = (new StringBuilder("67890"), 3);
Console.WriteLine(original);
Console.WriteLine(builder);
它等价于如下代码:
StringBuilder builder = new StringBuilder("12345");
StringBuilder original = builder;
StringBuilder targetForLength = builder;
(StringBuilder, int) tuple = (new StringBuilder("67890"), 3);
builder = tuple.Item1;
targetForLength.Length = tuple.Item2;
Console.WriteLine(original);
Console.WriteLine(builder);
12.1.3 元组字面量分解的细节
类似于11.3 元组类型及其转换,元组的分解也有完全相同的机制。如下代码合法:
// 分解
(string text, Func<int, int> func) = (null, x => x * 2);
// 赋值
(text, func) = ("text", x => x * 3);
// 隐式转换
(byte x, byte y) = (5, 10);
12.2 非元组类型的分解操作
非元组类型的分解,是一种基于模式的方式(类似于 async/await 和f oreach)。任何具有合适的 Deconstruct() 方法或扩展方法的类型,都可以使用与元组相同的语法完成分解。
12.2.1 实例分解方法
分解操作所用的 Deconstruct() 实例方法需要满足以下几条规则:
- 分解方法对于执行分解操作的代码必须是可 访问 的。(如果所有代码都位于同一个程序集中,那么可以使用 internal 来修饰
Deconstruct() 方法。) - 该方法的返回值必须是 void 。
- 参数个数不少于 2 (因为不可能分解一个值)。
- 该方法 不能 是泛型方法。
为什么
Deconstruct() 方法要通过 out 参数,而非返回元组?通过 out 参数可以进行重载,以分解不同的多组值。通过返回元组则无法实现该功能。
下面是一个 Deconstruct() 实现的简单示例:
public void Deconstruct(out double x, out double y)
{
x = X;
y = Y;
}
还可以通过表达式体的方式实现:
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
12.2.2 扩展分解方法与重载
Deconstruct() 扩展方法的使用规则,自然和实例方法保持一致:
- 需要对于调用代码可 见 ;
- 除了第一个参数(扩展方法的目标类型),其他参数必须是 out 参数;
- out 参数的个数不少于 2 ;
- 方法 可以 是泛型,但只有扩展方法的调用目标(第一个参数)才能参与类型推断。
下面是一个简单的示例:
static void Deconstruct(this DateTime dateTime, out int year, out int month, out int day) =>
(year, month, day) = (dateTime.Year, dateTime.Month, dateTime.Day);
12.2.3 编译器对于 Deconstruct 调用的处理
以如下代码为例,其中 target 不是元组实例,编译器会将其转为右侧代码:
(int x, string y) = target;
target.Deconstruct(out var tmpX, out var tmpY);
int x = tmpX;
string y = tmpY;
之后编译器按照常规方法的调用规则查找合适的方法。
这里的重点是:源码中声明的两个变量类型其实并没有用于 Deconstruct() 方法调用,即这两个变量并没有参与类型推断。这一事实解释了如下 3 个问题:
-
Deconstruct 实例方法 不能 是泛型方法,因为泛型方法无法为类型推断提供有效信息。 -
Deconstruct 扩展方法 可以 是泛型方法,因为编译器可能根据 target 这个类型实参做出类型推断,而且 target 也是类型推断的唯一信息来源。 - 在
Deconstruct() 方法进行重载时, out 参数的个数 是决定性因素,而非 参数类型 。如果再添加一个和已有方法 out 参数个数相同的重载方法,编译器会因为无法决定应该调用哪个方法而终止编译。
12.4 C#7.0 可用的模式
12.4.1 常量模式
常量模式由编译时常量表达式组成。输入类型有 2 种:
- 整型表达式:使用
== 进行比较 - 其他类型:使用
object.Equals() 静态 方法进行比较。
以如下代码为例,因 Match() 方法接受 object 类型的变量,传入整数值时发生了 装箱 !因此这些匹配实际使用的是 object.Equals() 方法 进行比较。故 Match(10L) 输出的内容是“ Input didn't match hello, long 5 or int 10 ”:
Match("hello");
Match(5L);
Match(7);
Match(10);
Match(10L);
static void Match(object input)
{
if (input is "hello")
Console.WriteLine("Input is string hello");
else if (input is 5L)
Console.WriteLine("Input is long 5");
else if (input is 10)
Console.WriteLine("Input is int 10");
else
Console.WriteLine("Input didn't match hello, long 5 or int 10");
}
12.4.2 类型模式
类型模式由一个类型和一个标识符组成。
类型匹配成功后会引入一个新的模式变量,该变量的类型为匹配成功的类型,变量值会被初始化为输入的值。
下面是类型模式的一个简单示例:
static double Perimeter(Shape shape)
{
if (shape == null)
throw new ArgumentNullException(nameof(shape));
if (shape is Rectangle rect)
return 2 * (rect.Height + rect.Width);
if (shape is Circle circle)
return 2 * PI * circle.Radius;
if (shape is Triangle triangle)
return triangle.SideA + triangle.SideB + triangle.SideC;
throw new ArgumentException($"Shape type {shape.GetType()} perimeter unknown", nameof(shape));
}
类型模式中,所指定的类型不能是可空值类型,以下代码 非 法:
static void CheckType(object value)
{
if (value is int? num)
{
}
}
但是所指定的类型可以是 类型形参 ,且 类型形参 在执行时可以是可空值类型:
CheckType<int?>(null);
CheckType<int?>(5);
CheckType<int?>("text");
CheckType<string>(null);
CheckType<string>(5);
CheckType<string>("text");
static void CheckType<T>(object value)
{
if (value is T t)
{
Console.WriteLine($"Yes! {t} is a {typeof(T)}");
}
else
{
Console.WriteLine($"No! {value ?? "null"} is not a {typeof(T)}");
}
}
12.4.3 var 模式
var 模式看起来与类型模式类似,区别是把 var 作为类型。其基本语法如下:
var 模式有如下特点:
- 它不检查任何内容,总能匹配成功
- 总会引入一个新的变量,其类型、值与输入完全相同
- 即便输入是 null 也能匹配成功
var 模式的最大用处是和 switch + 哨兵 语句搭配使用。下面是一个简单的示例:
static double Perimeter(Shape shape)
{
switch (shape ?? CreateRandomShape())
{
case Rectangle rect:
return 2 * (rect.Height + rect.Width);
case Circle circle:
return 2 * PI * circle.Radius;
case Triangle triangle:
return triangle.SideA + triangle.SideB + triangle.SideC;
case var actualShape:
throw new InvalidOperationException($"Shape type {actualShape.GetType()} perimeter unknown");
}
}
12.5 模式匹配与 is 运算符的搭配使用
模式匹配引入的模式变量有如下特点:
- 在 is 表达式中声明的模式变量,其作用域为 整个闭合块 。
- 如果某处使用模式变量的代码发生了编译错误,则说明在此处编译器还无法确定该变量是否已经被 赋值 。
关于作用域,以如下代码为例,text 的作用域二者相同:
string text = input as string;
if (text != null)
{
Console.WriteLine(text);
}
if (input is string text)
{
Console.WriteLine(text);
}
关于确定赋值,仅在模式变量确认已赋值的情况下,该模式变量才可用。以如下两段代码为例,第二段代码不确认是 x 还是 y 进行了赋值,因此 无法通过编译 :
if (input is int x && x > 100)
{
Console.WriteLine($"Input was a large integer: {x}");
}
void DoSomething(object input)
{
if ((input is int x && x > 100) || (input is long y && y > 100))
{
Console.WriteLine($"Input was a large integer of some kind: {x}, {y}");
}
}
12.6 在 switch 语句中使用模式
switch 模式的一个简单示例如下:
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 * PI * circle.Radius;
case Triangle tri:
return tri.SideA + tri.SideB + tri.SideC;
default:
throw new ArgumentException(...);
}
}
12.6.1 哨兵语句
每个 case 标签都可以有一条哨兵语句,该语句由一个表达式组成:
该表达式最终要计算出一个 布尔 值。case 标签下的语句,只有在该表达式计算为 true 时才会执行。此处的“when”便是哨兵。
在使用哨兵语句时,因其有可能返回 false,同一模式出现多次是常事。下面的代码演示了同一模式出现多次:
private string GetUid(TypeReference type, bool useTypeArgumentNames)
{
switch (type)
{
case ByReferenceType brt:
return $"{GetUid(brt.ElementType, useTypeArgumentNames)}@";
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 "(This part of the real code is long and irrelevant)";
default:
return type.FullName.Replace('/', '.');
}
}
12.6.2 case 标签中的模式变量的作用域
switch-case 中的变量可以分为两种:
-
在 case 体中定义的变量:作用域是 整个 switch 语句 。
-
在 case 标签中声明的变量:作用域 仅限当前 case 体 。
该情况下的变量主要有三种:
- 模式引入的变量
- 哨兵语句中声明的变量
- out 变量(见14.2 out 变量)
但这又迎来一个新的问题:基于模式的 switch 语句应当和普通 switch 语句一样,允许多个 case 标签共享同一个 case 体。此时 case 标签中声明的变量不能 重名 。相应的,因为编译器不确定会匹配到哪个标签、哪个模式变量会完成赋值,因此模式变量只能在 哨兵语句 中使用,而不能在 case 体 中使用。
下面是一个简单的示例:
static void CheckBounds(object input)
{
switch (input)
{
case int x when x > 1000:
case long y when y > 10000L:
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;
}
}
12.6.3 基于模式的 switch 语句的运算顺序
绝大部分情况下,基于常量的 switch 语句中的 case 标签 可以 任意排列,而 不会 影响代码的执行行为。
基于模式则完全不同,它的逻辑运算顺序可以概括为:
- 每个 case 标签都按照 源码 的顺序进行运算;
- 只有 当所有 case 标签都 经过运算后,default 标签的代码才会被执行,与 default 标签在 switch 语句中的 位置 无关。
以如下代码为例,即使 default 块处于开头,仍在最后匹配:
CheckBounds(1);
static void CheckBounds(object input)
{
switch (input)
{
default:
Console.WriteLine("default 块");
break;
case int x when PrintMsg("第一个 case 标签"):
Console.WriteLine("第一个 case 块");
break;
case int y when PrintMsg("第二个 case 标签"):
Console.WriteLine("第二个 case 块");
break;
}
}
static bool PrintMsg(string message)
{
Console.WriteLine(message);
return true;
}

浙公网安备 33010602011771号