C# 属性模式

原址:C# 属性模式 - 哔哩哔哩 (bilibili.com)

1、语法
属性模式是用于专门体现对象的属性信息的匹配模式。我们使用一对大括号来表达参数是否必须满足这个数值信息。

假如,我们现在的 Point 类型的 X 和 Y 不再使用字段表达,而是用属性来表达:

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
}

那么,我们即使不给出解构函数,也可以使用属性的方式来对每一个成员信息进行判断:

if (point is { X: 30, Y: 30 })
    // ...

属性模式专门给属性提供数据判断的服务,因此这种模式叫属性模式。

2、属性模式的弃元
一般来说,属性模式下,由于不需要依赖于解构函数,因此属性是可以写出来判断的;反过来说,如果属性不判断的话,那么写出来就没意义了。不过 C# 的语法允许我们使用弃元来默认通过某个属性的判定:

if (point is { X: 30, Y: _ })
    // ...

这样的话,Y 属性是永真式,即不用判断了。说白了,这里的 Y: _ 是可以不写的。只是 C# 允许这种语法存在,体现出了语法的灵活性。

3、空属性模式及变量声明内联
如果属性模式里的成员为空,那么它表示什么呢?

if (nullable is { } point)
    // ...

是的,对于可空类型(不管是值类型也好,还是引用类型也好),都表示“不为 null”。比如 nullable 是一个可空的 Point 类型,那么 is { } 就表示 nullable.HasValue。当满足条件后,我们用 point 表示这个 Point 类型的数据。

从这个例子里,我们可以得到的若干信息是这些:

is { } 表示“不为 null”,适用于任何可为空的类型;

大括号后可继续内联一个变量,和 is T variable 写法格式(声明模式)一致,但是,注意内联的这个变了和原始变量的类型和可空语义的不同:被匹配的变量(原始变量)是可空的,但是内联的后者这个变量是一定不空的。

C# 是允许变量声明的内联作为模式匹配的一部分的。这里仅用空属性模式介绍了内联变量的写法,但你要知道的是,内联变量可用在任何情况下的属性模式。

4、尽量不要让本来就不为 null 的表达式使用属性模式
可以发现,is 的左边其实可以为一个表达式。因此下面的代码是合法的:

if (new Student("Sunnie", 25) is { } student)
    // ...

不过,这种写法具有副作用。is 的左边一定是一个不为 null 的表达式,那么我们就没有理由使用 is { } 来进行模式匹配。因为这样会导致编译器生成不必要的判空代码。

因此,为了避免这样的写法出现,我们可以改成 var 模式,或者是直接定义一个新的变量来进行赋值。

// Way 1.
var student = new Student("Sunnie", 25);
// ...

// Way 2.
if (new Student("Sunnie", 25) is var student)
    // ...

请注意。这里所说的属性模式不单单只是空属性模式。在里面带别的属性使用 var 模式的话,也是不必要的写法。

var student = new Student("Sunnie", 25);
if (student is { Name: var name, Age: var age })
    // ...

这种写法看似是在直接使用大括号语法来同时获取两个属性的数值,但是如果 Student 是引用类型的话,属性模式的大括号本身会让编译器自动生成判空代码,于是这样的代码等价于 !ReferenceEquals(student, null) && student.Name is var name && student.Age is var age。是的,它会做一次判断 null 的冗余操作。

如果你需要对多个这样的属性一齐取值的话,我建议你使用值元组来进行赋值:

var (name, age) = (student.Name, student.Age);

用这样的语法来代替原来的写法。这样的赋值和原始的赋值的期望结果是一致的,但代码里也不会多出冗余的判空。

5、可空值类型模式匹配是匹配的内部数值
判别对象是否为空,我们可以使用 is null 来完成,因此不空就使用 !(obj is null) 就可以了;与此同时,由于空属性模式也可以完成相同的行为,因此这样的代码也可以写成 obj is { };对于可空值类型来说,我们还可以使用 HasValue 属性来完成:obj.HasValue。

但是,可空值类型在模式匹配里是当成值类型来假设的——它可能含有数值,那么数值直接拿出来即可;如果不含有数值,返回 null 就是判断模式的结果。而这里的 HasValue 是对所有可空值类型都具备的一个独特特性。但是在模式匹配里,你无法这么写代码:

if (nullableValueObject is { 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# 编译器确保了我们的操作完全只包含解构行为的时候,是可以不做判断即可使用这些变量的。举个例子。

_ = stu is { Name: var name, Age: var age };

它不依赖于你的解构函数:只要对象具有该属性数值且包含 get 访问器可以用于取值操作,这个属性就可以用来作为属性模式解构操作的一部分。这种解构形式和之前学到的解构函数的解构模式不同,这里用的是属性模式的方式获取,因此称为属性模式解构(Property-pattern-styled Deconstruction)。

另外,上面用到了弃元符号。因为 is 表达式不可单独使用,它必须返回数值给变量调用。如果你确实不使用结果变量(实际上这个解构行为根本就不可能失败,所以上面这样的 is 表达式永远返回 true)赋值给等号左侧的话,只需要写弃元符号即可,它等价于这样:

var name = stu.Name;
var age = stu.Age;
又或者是

var (name, age) = (stu.Name, stu.Age);
等等写法。

另外,这样的解构风格允许你包含弃元模式嵌套在属性模式之中。但凡右侧 100% 是成功的解构操作的话,你怎么写模式匹配都可以:

_ = obj is { A: _, B: { Nested: var nested, SecondNested: _ } _, C: var propCValue };

这些都是编译器允许的写法。这种就是带有递归使用的解构,它也是编译器允许的,因为这样的解构操作肯定是成功的。否则,由于可能失败,所以带有别的模式匹配的话,你可能就得用 if 来判断一下才知道是否模式匹配成功了。

if (obj is { A: 10, B: { Nested: var nested, SecondNested: _ } _, C: var propCValue })
    // ...

这样的话,由于 A 属性判断了数值,所以可能解构操作不成功,这种场合你只能使用 if,而且不能简化成上面属性模式风格的解构的样式。顺带一说,_ = a is pattern 表达式的 _ 不是模式匹配,它只是表示变量我们不使用了。

7、递归模式Ⅰ:属性模式递归
C# 强大的地方在于,语法很灵活,这样我们写代码可以不用唯一的一条道路去实现。比如前面的解构模式。(x: var x, y: var y) 里又是一个 var 模式的变量声明。所以,正是因为这样,我们学 C# 就不必学得那么痛苦。

C# 的属性模式是 C# 一大秀儿语法。它允许递归使用属性模式进行判断。假设我有这么一个对象:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Gender Gender { get; set; }
    public Person? Father { get; set; }
    public Person? Mother { get; set; }
}

这个对象是表示一个人的基本数据信息,比如名字啊、年龄啥的,当然也存储了 ta 的父母的实例的引用。

其中,我们假设 Gender 类型是个暂时只包含 Male 和 Female 俩字段的枚举类型。

Person? 语法表示 Person 这个引用类型具有和值类型类似的语法:这个属性信息可为 null。反之,如果没有 ? 标记的类型,这个成员的数值就不能为 null。这个语法是 C# 8 里的,这里为了体现出判断用法,故意写上了 ? 来表达为 null、更显眼一点;另外,这里故意取可为 null 的写法,还有一个目的,是为了体现一会儿模式匹配的语义,所以请不要和现实世界进行对比或者对号入座。

假如,我们要判断是否某个人的姓名是“张三”、年龄 24,他爸叫“张二”、而他的妈妈则叫“李四”。如果要判断这个对象的具体信息,我们可以这么写代码:

if (
    zhangSan is
    {
        Name: "Zhang San",
        Age: 24,
        Father: { Name: "Zhang 'er" },
        Mother: { Name: "Li si" }
    }
)
{
    Console.WriteLine("Zhang san does satisfy that condition.");
}

注意这里的模式匹配写法。前面模式匹配就用的是大括号,因此我们可以对对象的内部信息继续作判断。比如 Father 和 Mother 属性又是一个 Person 类型的对象,因此我们还可以接续一个大括号对 Father 和 Mother 的值的具体内容继续进行判断。

一定要注意。Father 和 Mother 属性是可能为 null 的。当 Father 属性的数值本身就是 null 的时候,那么显然就不存在 Name: "Zhang 'er" 的判断行为了:因为 null 值本身就无法继续判断内部数据了。因此,在 Father 为 null 的时候,模式匹配结果一定是 false。当且仅当整个判断的逻辑全都匹配,if 条件才成立。

顺带给大家看下,C# 的模式匹配到底多有魅力:给大家展示一个我之前写过的一段代码,用到了这里的模式匹配。

if (
    node is
    {
        Expression: MemberAccessExpressionSyntax
        {
            RawKind: (int)SyntaxKind.SimpleMemberAccessExpression,
            Expression: MemberAccessExpressionSyntax
            {
                RawKind: (int)SyntaxKind.SimpleMemberAccessExpression,
                Expression: IdentifierNameSyntax
                {
                    Identifier: { ValueText: "TextResources" }
                },
                Name: IdentifierNameSyntax
                {
                    Identifier: { ValueText: "Current" }
                }
            },
            Name: IdentifierNameSyntax
            {
                Identifier: { ValueText: var methodName }
            } nameNode
        },
        ArgumentList: var argList
    }
)
{
    // ...
}

这里,这么一大坨都是递归的模式匹配。正好这体现出了模式匹配的魅力。

8、递归模式Ⅱ:对位模式和属性模式是可以放在一起的
C# 的属性模式具有和对位模式完全一致的判断行为,因此 C# 就把对位模式和属性模式在语义分析上放在了一起。假设我有一个 Point 类型,包含 X 和 Y 属性(它们通过解构函数解构为 x 和 y 两个参数),并且包含 Area 属性表示当前点到坐标原点构成的矩形的面积。

这里不是讲数学,我只是告诉你如何并用两个模式。

if (point is (x: 10, y: 30) { Area: _ }) ;

可以看到,我们直接在 (x: 10, y: 30) 这个对位模式后加上了 { Area: _ } 属性模式。在 C# 里,对位模式和属性模式均可以用于递归使用(比如假设一个对位模式的成员是可以继续通过别的模式进行匹配的,那么这个成员就可以继续递归地进行模式的判断),同时属性模式也是如此,前文已经说过了。因此,C# 把对位模式和属性模式统称递归模式(Recursive Pattern)。换句话说,在概念上来讲,你可以同时使用对位模式和属性模式的两种不同模式的判别,并放在一起,这个整体叫做递归模式。

但请注意,必须是先对位模式,后属性模式的顺序。写反了是不行的。

 作者:SunnieShine https://www.bilibili.com/read/cv21216615 出处:bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值