C# 模式匹配的综合应用
下面我们来讨论一些关于前面讲解的模式匹配的综合内容。
1、如何判断集合至少有一个元素
如下的这些模式都可以。
:先看内层
not { } or []
,它表示要么not { }
,要么[]
。not { }
等于is null
,而[]
等于Length == 0
属性判断,所以对整个模式取反not (not { } or [])
就是not (null or { Length: 0 })
,即不空且至少包含一个元素;{ Length: not 0 }
:最基础的属性模式判断。不过这个用法稍微注意一下,如果集合不含Length
属性而是Count
属性,你可能需要改掉这里的Length
名称;not (null or [])
:not { }
就是null
,所以和第一个写法等价;{ Length: > 0 }
:和第二个一致,只不过这里是要求Length
必须非负。但是 C# 11 开始模模式必须非负,因此语义上> 0
和not 0
是一致的了;{ } and not []
:{ }
是not null
,not []
则是Length == 0
的属性判断,因此结合起来就是序列不空,且至少有一个元素;[.., _]
和[_, ..]
:这两个写法是等价的,不必多说——列表模式包含一个弃元符号和范围记号,因此序列至少包含一个元素才满足该要求,不过先写..
还是先写_
都无所谓,因为编译器都能识别。
整体来说,看你个人喜好来书写代码。它们最终是一样的代码,都是序列不空且至少有一个元素。
2、三种括号一起用
假设我们有一个类型,它使用了这样的模式匹配:
根据前面的知识,你可以猜测或者推断出,该类型的最小实现逻辑吗?换言之,你知道它允许这些模式匹配同时使用的时候,至少有多少个必需的成员存在呢?我们来想一想。
首先是 ()
。()
代表的是它是一个元组,但不包含任何数值。如果要找到“最优解”,只需要表示出 inst
变量是不是 ITuple
的实现类型就可以了。只要它实现自 ITuple
接口,那么对象就可以支持和兼容 ()
模式,不需要定义任何新成员。哪怕它不走 ITuple
派生,只要 inst
是 object
类型,那么这种模式匹配就是成功兼容的,就不会出现语法错误。
其次是 []
。列表模式要求对象至少是集合类型,那么对象至少有一个带 int
单参数的索引器,以及一个 Length
或 Count
属性的其中一个即可。那么,这至少就需要对象有两个成员的实现。
最后是 {}
。属性模式的唯一要求是,该类型不能是指针。因为指针类型永远都不包含任何判断属性。它只能取出其中的数值(*p
)然后才可能有对应的属性。因此为了尽量包容和兼容前面的模式,那么我们假设 inst
此时是 object
类型。那么由于它不是指针,因此属性模式就可以使用(即使我们知道 object
里不包含任何可访问的属性信息)。
所以,要想满足三种括号一起使用的模式匹配的话,那么至少需要对象从语法上实现两个成员(Length
或 Count
其一,然后一个 int
类型的单参数的索引器),然后 inst
是 ITuple
的实现类型即可:
3、过于复杂的递归模式匹配
考虑一种极端情况:
这意味着什么?这意味着我在使用解构模式的时候会产生这样的代码:
这个 (({}))
是一个嵌套模式,不过没什么特殊意义。我们拆开看看就明白了。首先 (({}))
最外层是一个 ()
模式,它表示对象可以解构就行,因此它等价于 s is not null
;然后里面一层是 ({})
的 ()
。它代表我在使用 Deconstruct
产生解构对象了之后又一次作判断。但 Deconstruct
是我写的一种极端代码:它解构了一个寂寞——返回了它自己。所以内层的 ({})
的 ()
还是跟原来判断信息完全一样。最后,最内层有一个空大括号,它表示空属性模式匹配,它依然和 o is not null
表达式等价,因此,完整的表达式和你写一个 s is {}
或 s is not null
没有区别。
你觉得好玩的话,我这还有一个例子。
然后模式匹配:
蛇皮怪。
我们强烈不建议你这么写代码,我之所以讲这个是为了告诉你有这么一种特殊的情况。而且,虽然没有这么多层级的嵌套,但经常会有括号嵌套括号的用法,两层还是蛮常见的:
((var a, var b), var c)
:将对象解构为两个值,用对位模式判断。其中第一个值可继续解构,并仍然使用对位模式继续对位判断;第二个值使用的是var
模式;([var a, var b], var c)
:将对象解构为两个值,用对位模式判断。其中第一个值使用列表模式判断;第二个值使用的是var
模式;({ Property: var a }, _)
:将对象解构为两个值,用对位模式判断。其中第一个值使用属性模式判断;第二个值使用的是弃元模式;[(var a, var b), var c]
:使用列表模式判断对象是否只有两个值。其中列表模式里的第一个值可解构为两个值,并都使用var
模式;第二个值使用的是var
模式;[[var a, ..], [.., var b]]
:使用列表模式判断对象是否只有两个值。其中第一个值可继续使用列表模式判断,并只判断该列表里的其中第一个值,使用var
模式;第二个值也使用列表模式判断,且只判断该列表里的最后一个值,使用var
模式;[{ P1: 42 }, [var p2], (p3: 0)]
:使用列表模式判断对象是否只有三个值。其中第一个值使用属性模式判断P1
属性;第二个值使用列表模式判断该列表是否只包含一个值,并使用var
模式将该值取出;第三个值使用对位模式判断p3
;{ Property: (var a, var b, _) }
:使用属性模式判断属性Property
。该属性的结果可继续使用对位模式判断三个值,前两个值都用var
模式,最后一个是弃元模式;{ Property: [var a, .., var b, _] }
:使用属性判断属性Property
。该属性的结果可继续使用列表模式,并判断第一个元素和倒数第二个元素,都用var
模式;{ Property: { Nested1: 42, Nested2: 0 } }
:使用属性模式判断属性Property
。该属性的返回值还可继续使用属性模式判断其中的Nested1
和Nested2
模式。