C# 属性模式
1、语法
属性模式是用于专门体现对象的属性信息的匹配模式。我们使用一对大括号来表达参数是否必须满足这个数值信息。
假如,我们现在的 Point
类型的 X
和 Y
不再使用字段表达,而是用属性来表达:
那么,我们即使不给出解构函数,也可以使用属性的方式来对每一个成员信息进行判断:
属性模式专门给属性提供数据判断的服务,因此这种模式叫属性模式。
2、属性模式的弃元
一般来说,属性模式下,由于不需要依赖于解构函数,因此属性是可以写出来判断的;反过来说,如果属性不判断的话,那么写出来就没意义了。不过 C# 的语法允许我们使用弃元来默认通过某个属性的判定:
这样的话,Y
属性是永真式,即不用判断了。说白了,这里的 Y: _
是可以不写的。只是 C# 允许这种语法存在,体现出了语法的灵活性。
3、空属性模式及变量声明内联
如果属性模式里的成员为空,那么它表示什么呢?
是的,对于可空类型(不管是值类型也好,还是引用类型也好),都表示“不为 null
”。比如 nullable
是一个可空的 Point
类型,那么 is { }
就表示 nullable.HasValue
。当满足条件后,我们用 point
表示这个 Point
类型的数据。
从这个例子里,我们可以得到的若干信息是这些:
is { }
表示“不为null
”,适用于任何可为空的类型;大括号后可继续内联一个变量,和
is T variable
写法格式(声明模式)一致,但是,注意内联的这个变了和原始变量的类型和可空语义的不同:被匹配的变量(原始变量)是可空的,但是内联的后者这个变量是一定不空的。
C# 是允许变量声明的内联作为模式匹配的一部分的。这里仅用空属性模式介绍了内联变量的写法,但你要知道的是,内联变量可用在任何情况下的属性模式。
4、尽量不要让本来就不为 null
的表达式使用属性模式
可以发现,is
的左边其实可以为一个表达式。因此下面的代码是合法的:
不过,这种写法具有副作用。is
的左边一定是一个不为 null
的表达式,那么我们就没有理由使用 is { }
来进行模式匹配。因为这样会导致编译器生成不必要的判空代码。
因此,为了避免这样的写法出现,我们可以改成 var
模式,或者是直接定义一个新的变量来进行赋值。
var
这种写法看似是在直接使用大括号语法来同时获取两个属性的数值,但是如果 Student
是引用类型的话,属性模式的大括号本身会让编译器自动生成判空代码,于是这样的代码等价于 !ReferenceEquals(student, null) && student.Name is var name && student.Age is var age
。是的,它会做一次判断 null
的冗余操作。
如果你需要对多个这样的属性一齐取值的话,我建议你使用值元组来进行赋值:
用这样的语法来代替原来的写法。这样的赋值和原始的赋值的期望结果是一致的,但代码里也不会多出冗余的判空。
5、可空值类型模式匹配是匹配的内部数值
判别对象是否为空,我们可以使用 is null
来完成,因此不空就使用 !(obj is null)
就可以了;与此同时,由于空属性模式也可以完成相同的行为,因此这样的代码也可以写成 obj is { }
;对于可空值类型来说,我们还可以使用 HasValue
属性来完成:obj.HasValue
。
但是,可空值类型在模式匹配里是当成值类型来假设的——它可能含有数值,那么数值直接拿出来即可;如果不含有数值,返回 null
就是判断模式的结果。而这里的 HasValue
是对所有可空值类型都具备的一个独特特性。但是在模式匹配里,你无法这么写代码:
HasValue
属性来完成属性模式匹配,这样的语法是错误的。因为编译器会假设 nullableValueObject
在模式匹配里是按数值进行判断的,即使它本身是可空值类型,但在模式匹配里它是被视为一个包含 null
的普通数值类型。比如说 a
是 int?
类型,那么 a is { HasValue: _ }
就是错误写法:因为 a
会被视为包含 null
的普通 int
类型,而不会被当成 int?
类型(即 Nullable<int>
类型)。这个意义在于,由于它进行模式匹配并不会被视为可空值类型,因此你无法使用 { HasValue: _ }
类似的模式来获取其结果。
如果确实要获取可空值类型的内部数据,你应该写 a is { } v
或 a.HasValue && a.Value is var v
,而不是 a is { HasValue: _, Value: var v }
。
6、用属性模式解构值类型对象
是的,C# 编译器确保了我们的操作完全只包含解构行为的时候,是可以不做判断即可使用这些变量的。举个例子。
get
访问器可以用于取值操作,这个属性就可以用来作为属性模式解构操作的一部分。这种解构形式和之前学到的解构函数的解构模式不同,这里用的是属性模式的方式获取,因此称为属性模式解构(Property-pattern-styled Deconstruction)。
另外,上面用到了弃元符号。因为 is
表达式不可单独使用,它必须返回数值给变量调用。如果你确实不使用结果变量(实际上这个解构行为根本就不可能失败,所以上面这样的 is
表达式永远返回 true
)赋值给等号左侧的话,只需要写弃元符号即可,它等价于这样:
又或者是
等等写法。
另外,这样的解构风格允许你包含弃元模式嵌套在属性模式之中。但凡右侧 100% 是成功的解构操作的话,你怎么写模式匹配都可以:
if
这样的话,由于 A
属性判断了数值,所以可能解构操作不成功,这种场合你只能使用 if
,而且不能简化成上面属性模式风格的解构的样式。顺带一说,_ = a is pattern
表达式的 _
不是模式匹配,它只是表示变量我们不使用了。
7、递归模式Ⅰ:属性模式递归
C# 强大的地方在于,语法很灵活,这样我们写代码可以不用唯一的一条道路去实现。比如前面的解构模式。(x: var x, y: var y)
里又是一个 var
模式的变量声明。所以,正是因为这样,我们学 C# 就不必学得那么痛苦。
C# 的属性模式是 C# 一大秀儿语法。它允许递归使用属性模式进行判断。假设我有这么一个对象:
这个对象是表示一个人的基本数据信息,比如名字啊、年龄啥的,当然也存储了 ta 的父母的实例的引用。
其中,我们假设
Gender
类型是个暂时只包含Male
和Female
俩字段的枚举类型。
Person?
语法表示Person
这个引用类型具有和值类型类似的语法:这个属性信息可为null
。反之,如果没有?
标记的类型,这个成员的数值就不能为null
。这个语法是 C# 8 里的,这里为了体现出判断用法,故意写上了?
来表达为null
、更显眼一点;另外,这里故意取可为null
的写法,还有一个目的,是为了体现一会儿模式匹配的语义,所以请不要和现实世界进行对比或者对号入座。
假如,我们要判断是否某个人的姓名是“张三”、年龄 24,他爸叫“张二”、而他的妈妈则叫“李四”。如果要判断这个对象的具体信息,我们可以这么写代码:
注意这里的模式匹配写法。前面模式匹配就用的是大括号,因此我们可以对对象的内部信息继续作判断。比如 Father
和 Mother
属性又是一个 Person
类型的对象,因此我们还可以接续一个大括号对 Father
和 Mother
的值的具体内容继续进行判断。
一定要注意。Father
和 Mother
属性是可能为 null
的。当 Father
属性的数值本身就是 null
的时候,那么显然就不存在 Name: "Zhang 'er"
的判断行为了:因为 null
值本身就无法继续判断内部数据了。因此,在 Father
为 null
的时候,模式匹配结果一定是 false
。当且仅当整个判断的逻辑全都匹配,if
条件才成立。
顺带给大家看下,C# 的模式匹配到底多有魅力:给大家展示一个我之前写过的一段代码,用到了这里的模式匹配。
这里,这么一大坨都是递归的模式匹配。正好这体现出了模式匹配的魅力。
8、递归模式Ⅱ:对位模式和属性模式是可以放在一起的
C# 的属性模式具有和对位模式完全一致的判断行为,因此 C# 就把对位模式和属性模式在语义分析上放在了一起。假设我有一个 Point
类型,包含 X
和 Y
属性(它们通过解构函数解构为 x
和 y
两个参数),并且包含 Area
属性表示当前点到坐标原点构成的矩形的面积。
这里不是讲数学,我只是告诉你如何并用两个模式。
可以看到,我们直接在 (x: 10, y: 30)
这个对位模式后加上了 { Area: _ }
属性模式。在 C# 里,对位模式和属性模式均可以用于递归使用(比如假设一个对位模式的成员是可以继续通过别的模式进行匹配的,那么这个成员就可以继续递归地进行模式的判断),同时属性模式也是如此,前文已经说过了。因此,C# 把对位模式和属性模式统称递归模式(Recursive Pattern)。换句话说,在概念上来讲,你可以同时使用对位模式和属性模式的两种不同模式的判别,并放在一起,这个整体叫做递归模式。
但请注意,必须是先对位模式,后属性模式的顺序。写反了是不行的。