C# 逻辑与括号模式
因为模式匹配里的每个模式并不是一个“数据信息”,因此我们无法直接对模式用 &&
、||
等符号来进行拼接组合。C# 为了解决这个问题,多了三个关键字:and
、or
和 not
来拼接模式。
1、合取模式
合取模式用 and
拼接模式,来表达这些模式都必须成立。
比如这里,>= 'a' and <= 'z'
整个表达式用来表达,>= 'a'
和 <= 'z'
两个条件必须都满足。如果要写分开,就必须写成 c is >= 'a' && c is <= 'z'
。
2、析取模式
析取模式用 or
拼接。
注意,or
拼接了前面 >= 'a' and <= 'z'
和后面 >= 'A' and <= 'Z'
两个模式。or
表示两个模式有一个模式能够匹配成功就可以。
这里我们介绍了一种新的语法:C# 允许模式匹配的内部使用小括号,来断开和分隔一个模式。and
和 or
的模式名称不变,但这个小括号套起来的模式,C# 称之为括号模式。
3、取反模式
取反模式用 not
。
最常见的就是这里。我们如果判断对象是不是不为 null
,那么我们最常用的就是写成 is not null
。is null
属于前面的常量模式,判断对象是不是 null
。它和 ==
运算符的区别是,==
运算符可重载,重载会影响 ==
的判断和使用逻辑;而 is
是永远不变的判断模式。
4、混用三种模式
当然,你也可以混用到 and
和 or
关键字拼接起来的模式里。
ch
not
这 4 行内容可以帮助你理解和拼接模式的具体内容。
5、三种模式的优先级和结合性
稍微注意一下。合取式 and
和数学上是一样的,比 or
更优先推理,因此无需对 and
和 or
模式一起的复杂模式匹配添加括号:
比如这样,(>= 'a' and <= 'z')
和 (>= 'A' and <= 'Z')
的小括号可以不要。
取反式的话,因为它只和一个模式结合使用,不像是 and
和 or
需要两个模式结合,因此 not
的优先级比 and
和 not
都要高。所以,上面的例子里,这个写法你应该是知道哪些地方省略了小括号。
6、对括号模式和对位模式下小括号的辨识
不知道你注意到了没有。由于我们这里介绍了三种逻辑模式的类型,因此我们也不得不会产生和使用一种新的模式来自定义模式的结合。可是,小括号在模式匹配里本身就有别的用途:它表示一个对位模式。
那么,如果我们出现了像是使用对位模式的语法,但它又可以被当成带括号的模式的话,这不就出现二义性了吗?
假设我们这里的 val
是一个对象。这里的模式 (3)
到底是什么呢?是括号模式里套了常量模式吗?还是说,它是一个一元组解构之后的对位模式呢?这我们无法确定,尤其是这个 val
刚好可以解构成这个样子的时候。
所以,考虑到语法的严谨性和兼容性,小括号一直以来都是被视为“冗余后可去掉”的存在,因此,(3)
模式会被认为是一个括号模式。而此时的 (3)
是一个很普通的常量模式,因此编译器会提示你可以直接去掉这个小括号。
不信你可以打开 Visual Studio 试一试。如果你在写一个对位模式匹配的时候,如果你写了一段代码:
当此时光标在 case ()
的 (
和 )
之间时,你肯定会往里面输入模式:先判断对象 a
,然后逗号,然后判断对象 b
。可是,你仔细看看,如果你在输入任何一个字符的时候,Intellisense 实际上并不是在给你提供 a
对象的属性成员让你对位匹配,而是比如 a:
啊、b:
啊之类的玩意儿。这是因为,编译器认为你这里的小括号是括号模式,因此括号可以不要。于是,它视为你正在对 (a, b)
这个元组类型 ValueTuple<T1, T2>
在做模式匹配。因此,它会提示给你看的是 a
字段和 b
字段(这两个字段就是 ValueTuple<,>
类型下自带的 Item1
属性和 Item2
属性在编译期间的临时引用名称。所以,Intellisense 给出的比如 a:
的玩意儿,实际上是让你填充进去 a:
的字段名,用于属性模式等模式的判断。
这就解释了前文“解构模式”里的一节内容:解构和对位模式不要求判断元素数量至少两个,以及单元素的解构模式要手动消除二义性,这两点的真实原因。
7、字面量在 and
模式下的类型可调整性
之所以放在这里说,是因为字面量是可以进行类型匹配的,这也就是前文提到的常量模式。不过,字面量有时候表现得并不一定非得是字面量本身的数据类型。
举个例子,1 是 int
类型的字面量,但我们可以使用 and
连接常量模式和类型模式,使得这个 1 的类型发生变化:
请注意这里的 uint and 1
模式。uint
是表示类型必须是 uint
类型,而 1 却又是 int
类型的字面量,这不会冲突吗?答案是并不会,字面量在模式匹配里会按照 and
里联立给出的类型进行隐式转换。如果能够转换过去,那么就是允许的。
举个例子,o is decimal and 1
和 o is decimal and 1M
是一个意思,这个字面量 1
可以写成 1M
但也可以直接写成 1
,因为字面量会按照类型匹配的关系自动转换其表达的类型。不过,这仅限于上下文可以暗示类型的情况。如果前面用的是 or
的话,就不行了。
8、is not var
组合模式到底是否为永假式?
所谓的永真和永假式,就是说这个式子的判断结果永远都是 true
或 false
。比如 if (true)
条件我们直接写的是 true
字面量,这就是一种典型的永真式;当然你不嫌复杂也可以写 if (!false && true || false || true)
这种超级复杂的写法。
那么,expr is var variable
的话,假设 expr
是一个表达式的话,那么它是否是永真式呢?
这个问题问得好。expr is var variable
的模式匹配规则允许我们将表达式的结果在内联为布尔运算逻辑的其中当成一个永真的表达式在使用,目的是为了合并多个布尔表达式,使之直接称为一个表达式还能跑起来,这样就可以不用使用一大堆的 if
判断语句来影响可读性和代码量了。
不过,is var
真的是永真的吗?如果是的话,那么 is not var
不就是永假式么?永假的逻辑在 if
里还不如就写成 if (false)
么?那这种东西还有何意义呢?
实际上,并非如此。is var
也不一定随时都永真。考虑一种情况:解构模式。解构模式需要我们按照一定的情况对一个对象进行解构。它需要对象有一个解构函数,或者是包含一个可访问到的扩展解构函数。
考虑一种情况,假设这个对象是引用类型呢?那么解构会随时都成功吗?不见得,对吧,因为它自己可能是 null
。因此,在引用类型使用该模式组合的时候是有别的含义的,比如:
对于这种情况下,nullableExpression
在判断的意思就是,如果它不可解构为后面的三个变量的时候,则退出方法。在这个时候,“不可解构”就对应了它为 null
的时候。
不过,对于纯定义变量的 is var
的话,它肯定是直接赋值过去的,它根本不判空。因此,is not var
不可用于这种情况:因为它永假,导致的情况就是无法完成匹配,因此 var
后面的变量你写啥都没有用:反正也不会成功赋值过去。因此后续的变量自然就不可能用得上它,因此编译器也不允许你这么写代码。
9、声明和 var
模式不能写进 or
模式的两侧
这里就需要稍微提及一下注意事项了。实际上,虽然模式匹配很好用,但有些时候我们难免会因为一些思维不严谨导致错误使用——本来我们觉得可以用,但实际上编译器不认为你这么写是严谨的,于是不允许你这么用。你还纳闷,为什么呢。
举个例子。倘若我们要定义一个变量,并且判断数值是否不为 0,那么我们可以这么做:
我们学了模式匹配后,就可以简化一下:
于是,我们使用 var val and not 0
来判断了数值结果。这是合理的。
但是,我们总有时候,会因为代码的书写逻辑冗长而不得不使用过滤思想:倒装判断逻辑,减少缩进,将所有的不满足条件的情况直接给 return
或 continue
提前退出,这样可以减少缩进。于是,我们就需要对上述表达式进行取反:
按照基本的取反规则,and
要转为 or
,not
会变为 not not
,也就是双重否定,即可以直接去掉;反之没有 not
的地方补一个 not
即可:
似乎可以这么写。但是,这样的转换真的合理吗?实际上是否定的。我们来详细说明一下,为什么 not var val or 0
是不正确的模式判断。
首先,not var val
模式指的是对 var
模式取反。var
模式唯一可以取反的情况只有元组判断的情况。如果一个引用类型用于解构,那么此时的 var
会优先判断一次 null
,毕竟对象不为 null
才可以进行对象解构。因此,这是唯一一种可以用 not var
的情况。但是,很显然这里的 is not var val
并非解构,它的 val
是一个临时变量,因此,not var val or 0
这个模式就永远不可能成立:因为 var val
是必然成立的赋值过程。
其次,就算我们退一万步讲,not var val
我们暂且不考虑赋值成功与否,or
代表的是两者至少有一个成立就可以。那么,如果我们假设 or
右侧的常量模式 0
成立的话,那么左边的 not var val
可能就不一定成立了。注意,这里我们要说“不一定成立”,是因为我们退了一万步在说明这个道理,这里是一个假设说明。那么,一旦这样的 var
模式不成立,我们就无法让编译器断定,这个 val
是否能在代码后面继续使用了。那么这里的 val
的声明就无法确保可用性。且不说编译器无法确定,就是你,在写代码的时候也不能确保能安全使用该变量。因此,对于声明模式和 var
模式下,这两种模式由于都会产生新的临时变量,因此为确保编译器可以正确知道变量的使用范围,我们不能将这两种模式放在 or
的两侧的其中一边。
那么,上面的代码也不是没有办法去做。你只需要将原始的模式加上小括号,然后在小括号左边加 not
表示对整个模式取反即可。
这就可以了。这样编译器就知道,你在匹配 var val and not 0
模式。当不满足的时候,if
条件成立,直接 return
;反之,var val and not 0
模式成立,于是 val
变量可以安全正确使用。
当然,这里需要你打上小括号。因为 not
优先级最前,它是单模式的运算,不打括号会被视为 not var val
和 not 0
两个模式的 and
连接,导致错误:not var val
是不可能成立的。
10、纯弃元不可单独使用
前文简要介绍过弃元模式的用法和范畴,不过弃元模式是可以直接运用到逻辑模式里的:
var _
或者别的什么模式,就可以直接使用纯弃元 _