C# 对象作为参数_C# 模式匹配

C# 模式匹配

啥是模式匹配?模式匹配是一种高端的使用机制,它允许程序员在开发的时候以对象的类型作为条件筛选和分情况处理的一种手段。

虽说模式匹配(Pattern Matching)这个说法有些高端,不过我们依旧不必害怕,术语词的高端导致我们无法理解它。

C# 7 开始支持模式匹配。如果需要使用如下的功能,请自行将项目调整到可以使用 C# 7 的框架版本上去。

从类型讨论说起

假设,我们拥有一个 Shape 的抽象数据类型,这个数据类型有 CircleRectangleSquareTriangle 等类类型将其抽象方法实现或继承下来。如果我们拿到的一个数据类型只是 Shape 类型的,那么我们如何去判断它具体是什么形状呢?一般的代码是这么写的:

Shape s = ...;
if (s is Circle)
{
    var c = (Circle)s;
    ...; // Code using `c`.
}
else if (s is Rectangle)
{
    var r = (Rectangle)s;
    ...; // Code using `r`.
}
else...

可以看出,这种书写格式没啥问题,但会让程序特别臃肿。后来,C# 允许用户直接把转换后的对象写在 is 类型判定的类型后,于是少了一个语句:

Shape s = ...;
if (s is Circle c)
{
    ...; // Code using `c`.
}
else if (s is Rectangle r)
{
    ...; // Code using `r`.
}
else...

这明显就少了很多个定义语句。不过,这个写法依旧臃肿,因为不同分支会添加大括号让代码变得很长。所以我们可以考虑用 switch 语句来代替。switch 语句最初也不允许直接类型判别,因为 switch 最开始是按数值作分类讨论的。自从 C# 7 诞生以来,switch 也支持了类型判定。于是写法变成了这样:

Shape s = ...;
switch (s)
{
    case Circle c:
        ...;
        break;
    case Rectangle r:
        ...;
        break;
    case ...
}

看起来好像差不多,不过更加清晰明了。

如上,这些都是模式匹配的基本操作,用到的是 is 来判别数据类型。接下来我们就来看看 is 的各种模式匹配的骚操作吧。

弃元和模式匹配

在 C# 7 里引入了一个概念,叫做弃元(Discard)。弃元表示一个我们完全用不上的数值,并用关键字 _ 表示和占位。

目前 _ 可以用在 out 参数、switch 的模式匹配标签、switch 表达式、is 模式匹配的转换参数名和返回值上。这里侧重要讲到的是这里的模式匹配标签(switch 语句)。

当我们在判断标签上用不上这个对象的时候,你可以使用弃元来占位:

switch (obj)
{
    case int _:
        return 1;
    case double _:
        return 2;
    ...;
}

另外,is 的模式匹配上,你可以使用弃元,也可以不写这个变量:

if (o is int _)
{
    ...;
}

if (o is int)
{
    ...;
}

完全等价。

这个弃元是出现在模式匹配之后的,所以最初就允许不写变量。之后出了弃元,才为了配合语义完整性,添加了弃元依旧成立的语法格式。
但是,这一点目前在 switch 语句里还不行: switch 标签上暂时还不允许省略弃元。

null 模式

最初,我们一直用的是 object.ReferenceEquals 方法来判断是否一个对象为 null

if (ReferenceEquals(a, null))
{
    ...;
}

当然,因为我们说,静态方法不可被重写,但依旧可以继承下来,所以我们在任何地方都可以使用这个方法,且能省略 object.ReferenceEqualsobject. 这部分。不过,这样写太长了,于是我们知道,object 里有一个自带的运算符 == 可以直接用,它直接调用的是这个方法。所以我们完全可以直接用 ==

if (a == null)
{
    ...;
}

那么问题来了。C# 允许用户自定义运算符的重载,可是 a 这个类型如果重新实现了 == 的操作的话,忘了判定 null 或者预期没有考虑到 null 传入的情况,那直接使用 == 就会直接定位到这个重载后的运算符 == 上,而不是 object==。所以我们就得给 a 或者 null 前增加强制转换:

if ((object)a == null)
{
    ...;
}
​
// Or
if (a == (object)null)
{
    ...;
}

这样依旧是可行的。不过,这么写太长了。所以 is 在 C# 7 允许用户直接书写来判断 null 情况。

if (a is null)
{
    ...;
}

语句就会少很多。

不要以为 null 是不能转换的。你错了, null 虽然在一般使用场景下都会触发 NullReferenceException,但强制转换并不会报错。 null 是可以允许被转换为任何引用类型或可空的值类型的。

不过,null 模式不支持再添加变量或弃元在后面。因为判断结果是 null 数值,根本没有必要单独再建一个变量来表示 null(你直接用原本的变量不就行了)。

废话模式

C# 2 引入了 var 类型。这是一个强类型,只是我们不想或不记得原本类型名称(可能很长也可能很短)的写法,于是我们索性在类型名位置放上 var 让 C# 编译器自己进行类型推断。随后,它也被拿来作为模式匹配了。

我们使用 is 习惯后,就经常会这么书写代码:

if (a is Rectangle r)
{
    int width = r.Width;
    if (width > 20)
    {
        ...;
    }
}

由于 width 变量必须被定义在大括号里,所以我们没办法放在 if 条件判断的那个小括号里面。后来,为了方便,C# 允许了用户把变量放在 if 里,不过怎么写呢?

假定这个 Width 属性是 int 类型的,那么我们可以把上面的写法(俩 if)合并成一个 if

if (a is Rectangle r && r.Width is var width && width > 20)
{
    ...;
}

虽然 if 长了,但是更清晰明了了。如果 aRectangle 类型的,且这个类型的 Width 属性大于 20。这里的 var 会被自动认为是一个 int,而这个写法(r.Width is var width)永远是为 true 的,因为这是一句看起来像是废话的语句。

虽然是废话,但是我们能写到 if 里,这样就更优雅了。

当然,我们刚说了,var 替代的类型是 int,所以我们也可以写 int

if (a is Rectangle r && r.Width is int width && width > 20)
{
    ...;
}

我喜欢称之为废话模式,因为没有类型转换,单纯是考虑可以把变量放到 if 条件里的一种格式。不过为了简化代码,废话模式也不是随时随地都是废话。

属性模式

普通写法

如果我们把上面的条件改一下,把 > 20 改为 == 20。这个写法就还能继续简写:

if (a is Rectangle { Width: 20 } r)
{
    ...;
}

这个写法等价于

if (a is Rectangle r && r.Width is int width && width == 20)
{
    ...;
}

仔细观察写法 Rectangle { Width: 20 } r。在类型的后面加上一个大括号,把需要判断的数据信息写在大括号里。注意这里暂时只能接受常量数值。另外,如果判断结果成功的话,r 就会被赋值,后续就可以使用了。

当然,我们可以写多个条件:

if (a is Triangle { A: 2, B: 3, C: 4 } t)
{
    ...;
}

类型判断

如果我们这里的大括号里写的是数据类型而不是一个直接的常量,这样是允许的吗?

答案是,C# 编译器考虑到了你所说的这个情况。

if (a is Triangle { A: int i } t)
{
    ...;
}

这里 a 除了判断它是不是 Triangle 类型外,还要看这个类型的 A 属性是不是 int 的。如果是的话,那么 a 会成功转换为 t 对象,且 i 也可以在大括号里用。

空大括号判断

另外,如果判断这个对象不是 null,我们还有一个骚操作写法:

if (a is { } o)
{
    ...; // `o` is not null.
}
​
// Reverse case
if (!(a is { } o))
{
    ...; // `o` is null.
}

我们不声明其数据类型的时候,会被认为这个对象就是原本的数据类型。比如这里,假设 aShape 的类型,那么属性模式的大括号里啥都不写,就会直接判断成功。当且仅当对象不是 null 的时候,这样的情况就一定会成立;反之,如果对象是 null,那么 is 本身的判断就不成立,更别说后面的大括号了。所以这样写就可以直接表达 onull 和不是 null

递归模式

大括号里是可以嵌套大括号判断的。如果我们判断的一个类型是 A 类型的数据,它有一个 B 类型的属性,且这个 B 类型里有一个 int 类型的整数字段,我们甚至可以嵌套大括号:

if (obj is A { Item: B { Value: 6 } } a)
{
    ...; // Code using `a`.
}

之所以叫递归模式,因为这个大括号可以无限往下。这个写法等价于

if (obj is A a && a.Item is B b && b.Value is 6)
{
    ...; // Code using `a`.
}

所以,你可以记住一点,这个大括号里写的 prop: value 的格式里,冒号就好比是外部的 is 关键字。不信你可以试试把这里的所有冒号替换为 is 再来读一下这个语句,看看是不是完全看不出问题。

元组模式

普通写法

有些时候我们会写元组类型,如果我们同时判断多个数据各是什么类型的话,可以考虑使用元组模式来简化书写:

(object, object) o = ...;
if (o is (int, double) pair)
{
    int z = (int)pair.Item1;
}

这里 (object, object)(int, double) 都是一个二元组,不要被表象迷惑了,以为它是其他的东西。

对位模式

当然,你依旧可以对每一个数据进行 null 的判别。

(object, object) o = (1, 2);
if (o is (null, null) pair)
{
    ...;
}

这种叫对位模式,上面的二元组类型有两个元素构成,所以下面作判定的时候,也是对位判断是否是 null。如果两个都是 null,那么这个二元组结果就可以用 pair 变量表示,并且在 if 里可以直接用 pair 了。

常量模式

还有一些时候,我们在不知道对象是什么类型的时候就想看它具体的数值,也为了避免为 null 的时候直接判断数据会报错。所以,is 后还可以加入常量判断来看对象是不是这个类型,且是不是这个数值。

object o = 3;
if (o is 14)
{
    ...;
}

这样等价于

object o = 3;
if (o is int i && i == 14)
{
    ...;
}

这样有一个好处。假如对象是 null 数值,if 条件就不成立,但不会抛异常。

这个写法经常用来判断三值布尔类型(Nullablebool? 类型)。假如有一个方法返回了可能是 truefalsenull 的三种情况的其一,我们为了避免 null 引用异常,我们可以写 is 看这个对象是否是具体的哪个数值。

if (a is true)
{
    ...;
}
​
// Or
if (a is false)
{
    ...;
}

不过请注意,三值布尔类型的相反情况和普通布尔类型有区别。!true 在普通的条件下,只能是 false;但在 bool? 下就可以是 falsenull 了。

逻辑模式

在 C# 9 里,模式匹配再度优化。C# 9 为开发人员提供了三个新的关键字 andornot,这三个关键字用处就大了,它们可以用在一些完全你想不到的地方上。

andis 判别多条件情况

1、基本用法

我们可以使用 and 对一个不知道是什么类型的数据做数据大小的判断:

object o = 16;
if (o is int i and > 60) // Here `i` is not used in the condition clause. You can remove it.
{
    ...;
}

这个写法有一点意思的地方在于,我们把 > 60 这个看似不知道是什么鬼的残缺语句直接放在 and 之后,前面是类型判别。有意思吧。当前,这个写法 C# 9 可以直接继续化简。

object o = 16;
if (o is > 60)
{
    ...;
}

呃,厉害了。

不要急。主要是这个 o 传入的目前是一个整数。如果它可能是小数呢?

if (o is float and > 50F)

当然能这么写了。

2、高级用法

我们前面写了一种写法:

if (a is Rectangle r && r.Width is int width && width > 20)
{
    ...;
}

后来,有了 and 关键字,我们就可以这么写了:

object o = new Rectangle { Width = 4, Height = 10 };
if (o is Rectangle and { Width: > 20 })
{
    ...;
}

惊喜不……现在连属性模式也可以用大小判断了!

仔细看 : > 还挺萌。
哦对,这里的 and 是可以不需要的。答案很简单,最初我们的判定写法就是 type { prop: value } 的格式,如果这个 and 没有了,就只是看作一个很普通的属性模式。只是没有到 C# 9 时,这个大括号里只能是一个常量,并不能判断大小关系。

or:由 || 变过来的写法

当如果我们要判断一个数据类型可以是多个的其一时,可以考虑使用 or 关键字。

if (o is byte or short or int or long)
{
    ...;
}

这样,o 如果是 byteshortintlong 的其一时,判断都是成功的。

当然,这个依然可以用数据判断。如果不同的数据类型,我们甚至可以配合 and 一起用:

if (o is > 10 and < 20 or > 10F and < 20F or > 10D and < 20D)
{
    ...;
}

o 是整数且范围在 10 到 20 之间,或是 float 类型的 10 到 20 之间,或是 double 类型的 10 到 20 之间,条件都成立。

not:判断取反

当我们要取反 is 判断的时候,我们不得不为其添加一个小括号,然后写一个取反运算符 !。这样太丑了,所以 C# 9 带来了一种简化写法:

if (o is not int i)
{
    ...;
}
​
...; // Here `i` is available which can be used.

这个写法稍微有点诡异,当然 not 我们能理解语义模型定义的位置,因为一般英语就习惯把 not 放中间。可是这个 i……

是的,这个地方有点麻烦。你不妨把它按照传统思维去理解它:

if (!(o is int i))
{
    ...;
}
​
...;

这样一写你就明白了,i 变量在是 int 的时候会有转换。但不是 int 的时候,这个大括号里是不允许使用 i 的;反之,出来之后,i 变为了可用状态。

not 不仅用于取反一个条件,还能判断数据范围。前面不是有一个类似 is >= 100 and <= 200 的写法吗?我们要想取反这个条件,按道理说是可以直接取反的(大于改小于等于,小于改大于等于,and 改成 or)。不过,有一个更好看的写法,就是把它们括起来然后加个 not

if (o is not (> 100 and < 200))
{
    ...;
}

这个写法看起来很诡异吧。但是前面的内容就已经知道了,这个 > 100< 200 本身看起来语句残缺,但它是模式匹配里常用的语句形式。

而且这样写有一个好处,数据校验。当数据不在范围的时候,直接抛异常。

if (age is not (>= 0 and <= 150))
{
    throw new ArgumentOutOfRangeException(
        paramName: nameof(age),
        message: "The age cannot be below 0 or greater than 150.");
}

当然你要不喜欢 not 也可以不要这个小括号和 not

if (age is < 0 or > 150)
{
    throw new ArgumentOutOfRangeException(
        paramName: nameof(age),
        message: "The age cannot be below 0 or greater than 150.");
}

它和传统的数据校验是一致的,不过原本的写法需要写两次 age 变量:

     ↓here      ↓here
if (age < 0 || age > 150)
{
    ...;
}

你可能感兴趣的:(C#,对象作为参数,模糊匹配null数据)