C# 8.0 中的模式匹配

Contents

  • 模式匹配简介
  • C# 8.0 中模式匹配的演变
  • 表达模式
  • 结语

多年来,我们在 C# 中实现了许多功能,不仅改善了代码的性能,更重要的是还提高了代码的可读性。鉴于软件行业的快速发展,语言当然需要与其用户群同步发展。广泛用于 Haskell、Swift 或 Kotlin 等各种编程语言的某些功能,有时也会用于 C#。其中一个功能就是模式匹配,这一概念已经存在很长时间,是 .NET 领域中的许多开发人员一直期待的功能。

从 C# 7.0 开始,开发人员就体验到了模式匹配的强大功能。我们见证了模式开始成形,然后变成非常强大且有趣的语言补充的过程。正如其他语言功能彻底改变了软件编写方式一样,我希望 C# 中的模式匹配也会产生类似效果。

不过,我们真的需要另一种语言功能吗?我们不能就使用传统方式吗?当然可以。尽管模式匹配等附加功能肯定会改变许多人编写代码的方式,但对于多年来引入的其他语言功能我们也可以提出同样的问题。

彻底改变 C# 语言的其中一个功能就是引入的语言集成查询 (LINQ)。现如今处理数据时,人们会按自己的喜好来进行选择。有些人选择使用 LINQ,在某些情况下,使用这种语法构造的代码不会那么冗长,而其他人则会选择传统循环程序。我预计模式匹配的应用情况也是类似的,因为随着开发人员逐渐抛弃更为冗长的方法,新功能将改变他们的工作方式。注意,由于许多开发人员会选择坚持使用经验证切实可行的解决方案,因此传统方法不会有什么发展。但其他语言功能应该会提供一种方法来补充 C# 代码项目,而不是排斥当前代码。

模式匹配简介

如果你曾经使用过 Kotlin 或 Swift 等语言,那么你可能已经见过模式匹配的实际示例。它广泛应用于市场上各种不同的编程语言。当然,主要是为了使代码更具可读性。那么什么是模式匹配?

它相当简单。你拿到一个给定结构,根据它的外观进行识别,然后你就可立即使用。如果你有一袋水果,你低头一看就能立即看出苹果和梨的区别。即使它们都是绿色的。与此同时,你可以低头看向水果袋,并确定哪些水果是绿色的,因为我们知道所有水果都有颜色。

区分水果类型和水果属性就是模式匹配的功能。开发人员在进行识别时会使用各种表达方式。

按照传统方法,我可以使用简单条件来检查所有水果。但如果我需要显式地使用苹果,那会发生什么情况呢?最后会演变为以下情况,我必须先验证类型、属性,然后强制转换为 apple。这段代码最后会变得有点混乱,坦率地说,它很容易出错。

在以下的示例中我将特定类型的水果验证为 apple。我应用了属性约束,然后为了使用它,我必须将其强制转换,具体如下:

if(fruit.GetType() == typeof(Apple) && fruit.Color == Color.Green)
{
    var apple = fruit as Apple;
}

我可以采用的另一种方法是使用 is 关键字,这种方法灵活性更高。与上一示例不同,is 关键字还会匹配派生的 apple

if(fruit is Apple)
{
    MakeApplePieFrom(fruit as Apple);
}

在这种情况下,如果 fruitapple 的派生类型,我就可以使用它来制作 apple pie。而在之前的示例中,它必须是特定类型的 apple

幸运的是,有一种更好的方法。如前所述,使用 Swift 和 Kotlin 等语言就可以使用模式匹配。就 C# 7.0 而言,它引入了轻量级版本的模式匹配,这很有帮助,尽管它缺乏许多其他语言提供的优秀功能。你可以将上述表达式重构到下方的 C# 7.0 代码,这样你就可以使用 switch 来匹配各种模式。它并不完善,但相较于之前的代码确实有所改进。代码如下:

switch(fruit)
{
    case Apple apple:
        MakeApplePieFrom(apple);
        break;
    default:
        break;
}

以下是一些值得注意的内容。首先,注意这段代码没有进行任何类型强制转换,并且我还可以在事例上下文中使用刚匹配的 apple。与 is 关键字一样,这也会匹配派生的 apple

相较于 C# 6.0 中的类似代码,这段 C# 7.0 代码可读性更好,而且更容易让会话生效。这段代码仅仅表示,“基于 fruitapple 的事实,我想使用这个 apple。” 每个事例都可以匹配具有相似特征的类型,举例来说,这意味着它们从相同的类继承,或者实现相同的接口。在此事例中,applepearbanana 都是 fruit

缺少的部分就是将绿色苹果筛选出来的方法。你见过异常筛选器吗?这是 C# 6.0 中引入的功能,使用它可在仅满足特定条件时捕获特定异常。此功能引入了 when 关键字,它也适用于模式匹配。我可以使用模式匹配来匹配 apple,并且仅在满足条件时输入 case。图 1 对此进行了展示。

图 1:使用 When 关键字来应用筛选器

Fruit fruit = new Apple { Color = Color.Green };
switch (fruit)
{
    case Apple apple when apple.Color == Color.Green:
        MakeApplePieFrom(apple);
        break;
    case Apple apple when apple.Color == Color.Brown:
        ThrowAway(apple);
        break;
    case Apple apple:
        Eat(apple);
        break;
    case Orange orange:
        orange.Peel();
        break;
}

如图 1 所示,顺序至关重要。我先寻找颜色为绿的苹果,因为这个特征对我来说是最重要的。如果还有另一种颜色,假设棕色,这表示我的苹果腐败了,我想把它扔掉。至于其他苹果,我不想用来制作派,所以我就吃了。最终的苹果就是既非绿色也非棕色的所有苹果。

你还会发现,如果我得到橘子,我就会剥掉橘皮。我并不局限于处理一种特定类型;只要这些类型都继承自 fruit,就都可以处理。

其他部分的运行方式与你自 C# 1.0 以来一直使用的普通 switch 相同。这个示例完全是使用 C# 7.0 编写的,那么问题来了,还有改进空间吗?我会说有。代码仍然有些冗长,可以通过改进模式的表达方式使其更具可读性。此外,它还有助于使用其他方法来表达对数据“外观”的约束。 接下来我们讨论 C# 8.0,并介绍为使生活更舒适而引入的更改。

C# 8.0 中模式匹配的演变

最新版本的 C#(目前为预览版)引入了一些重要的模式匹配改进。若要试用 C# 8.0,必须使用 Visual Studio 2019 预览版,或者在 Visual Studio 2019 中启用预览版语言功能.C# 8.0 将于今年下半年正式发布,预计与此同时 .NET Core 3.0 也将推出。我们如何寻找新的方法来表达对属性类型的约束?我们如何使块模式的表达式更为直观、可读性更强?在 C# 8.0 中,该语言又向前迈进了一步,引入了可与各种模式配合使用的方法,而使用过 Kotlin 等语言的人应该非常熟悉这些模式。这些都是使代码可读且可维护的附加功能。

首先,我们现在可以使用称为 switch 表达式的代码,而不是开发人员自 C# 1.0 以来就一直使用的传统 switch 语句。下面是 C# 8.0 中 switch 表达式的示例:

var whatFruit = fruit switch {
    Apple _ => "This is an apple",
    _ => "This is not an apple"
};

如你所见,不必为每个不同的匹配编写事例和断点,只需使用模式和表达式。匹配 fruit 时,下划线 (_) 表示我不在意我所匹配的实际 fruit。事实上,它不必是 fruit 的初始化类型。下划线还会匹配 null。将其视为简单匹配特定类型。发现这个 apple 时,我使用与 C# 6.0 中引入的表达式体成员非常相似的表达式返回字符串。

这不仅仅是保存字符。请考虑这种可能性。例如,我现在可以引入表达式体成员,其中包含这些 switch表达式中的某一个,它还利用了模式匹配的强大功能,如下所示:

public Fruit Fruit { get; set; }
public string WhatFruit => Fruit switch
{
    Apple _ => "This is an apple",
    _ => "This is not an apple"
};

代码会变得非常有趣且功能强大。以下代码展示你会如何以传统方式执行此模式匹配。看一看,然后选定更喜欢的那个代码:

public string WhatFruit
{
    get
    {
        if(Fruit is Apple)
        {
            return "This is an apple";
        }
        return "This is not an apple";
    }
}

显然,这是一个非常简单的场景。假设引入约束时,我要匹配多个类型,然后在条件上下文中使用强制转换类型。已经对这个想法感兴趣了?我想也是!

虽然这种语言补充很受欢迎,但请抑制冲动,勿对每个 if/else if/else 条件都使用 switch 表达式。至于错误示例,请查看以下代码:

bool? visible = false;
var visibility = visible switch
{
    true => "Visible",
    false => "Hidden",
    null, _ => "Blink"
};

这段代码表示,你为一个可为 null 的布尔值使用了四个事例,这种用法当然是错误的。试着注意如何使用 switch 表达式,并且就像使用任何其他语言功能一样,不要滥用语法。

我已经介绍了 switch 表达式可以减少代码编写量并使代码更具可读性这一事实。这一点在为类型添加约束时也是如此。如果你查看元组、解构和所谓的递归模式的组合,C# 8.0 中对模式匹配的更改就会非常明显。

表达模式

递归模式是指一个模式匹配的表达式的输出变为另一个模式匹配的表达式的输入。这意味着解构对象,并查看对象类型、对象类型的属性、对象类型的属性的类型等的表达方式,然后应用所有上述内容的匹配。这看似复杂,但实际上并不复杂。

接下来介绍一种不同类型及其结构。在图 2 中,你将看到继承自 ShaperectangleShape 只是一个引入了属性点的抽象类,而属性点是将 shape 放到表面上的方法,通过它我就知道所放置的位置。

图 2 解构示例

abstract class Shape
{
    public Point Point { get; set; }
}
class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public void Deconstruct(out int width, out int height, out Point point)
    {
        width = Width;
        height = Height;
        point = Point;
    }
}

你可能想了解图 2 中的解构方法究竟是什么。使用它我可以将实例的值“提取”到类以外的新变量中。它通常与模式匹配和元组一起使用,稍后你会发现这一点。

因此,我基本上有三种在 C# 8.0 中表达模式的新方法,而且每种方法都有特定用例。它们是:

  • 位置模式
  • 属性模式
  • 元组模式

不必担心,如果你更喜欢常规的 switch 语法,你也可以将其与这些模式匹配改进配合使用!模式匹配方面的这些对语言的更改和补充通常称为递归模式。

位置模式使用你的类上的解构方法。你可以表达与给定值相匹配的模式,而该值是通过解构获取的。鉴于你定义了解构 rectangle 的方法,你可以表达一个模式,该模式使用输出(图 3 中所示)的位置。

图 3 位置模式

Shape shape = new Rectangle
{
    Width = 100,
    Height = 100,
    Point = new Point { X = 0, Y = 100 }
};
var result = shape switch
{
    Rectangle (100, 100, null) => "Found 100x100 rectangle without a point",
    Rectangle (100, 100, _) => "Found 100x100 rectangle",
    _ => "Different, or null shape"
};

首先,匹配 shape 的类型。在此示例中,我只想将其与 rectangle 匹配。第二个应用的模式在与 rectangle 匹配时,配合使用解构方法和元组语法来表达我在每个特定位置所需要的值。

我可以指定我明确希望该点为 null,或者可以使用下划线来表达我根本不在意。记住,此处顺序非常重要。如果在我们的版本中我们并不在意顶点,那么无论rectangle是否具有点,它始终都会与该模式匹配。这称为位置模式。

如果可以使用解构函数,这就非常方便,即使解构函数输出很多值,导致变得相当冗长。这就是属性模式发挥作用的点。到目前为止,我已经匹配了各种类型,但某些场景要求匹配 state 等其他类型,或者要求只查看各种属性值或其中缺少的属性值。

如以下代码所示,只要我获得的结果与包含点的类型匹配(其中这个点的 Y 属性值为 100),我并不在意结果的类型:

shape switch
{
    { Point: { Y : 100 } } => "Y is 100",
    { Point: null } => "Point not initialized",
};

注意,代码实际上并不处理 shape 为空或点已初始化但 Y 值不为 100的情况。在这些情况下,这个代码会引发异常。这可以通过使用下划线引入默认事例来解决。

我还可以确切地说,我需要该点未进行初始化,并且我只处理那些未初始化的场景。这比使用位置模式要简洁得多,而且在无法向所匹配类型添加解构方法的情况下,非常有效。

最后,我还有可以使用位置模式的元组模式,并且使用它我可以组合用于运行匹配的元组。我可以用一个场景来说明这一点,在这个场景中我根据开门、关门以及锁门等不同状态进行操作(见图 4)。根据门的当前状态、我要执行的操作以及我可能拥有的钥匙,可能会出现特定的情况。使用元组模式引入状态计算机的这个示例是 C# 设计主管 Mads Torgersen 经常使用的示例。请访问 bit.ly/2O2SDqo 阅读 Torgersen 的帖子“在 C# 8.0 中使用模式执行更多操作”。

图 4 元组模式

var newState = (state, operation, key.IsValid) switch
{
    (State.Opened, Operation.Close, _)      => State.Closed,
    (State.Opened, Operation.Open, _)       => throw new Exception(
        "Can't open an opened door"),
    (State.Opened, Operation.Lock, true)    => State.Locked,
    (State.Locked, Operation.Open, true)    => State.Opened,
    (State.Closed, Operation.Open, false)   => State.Locked,
    (State.Closed, Operation.Lock, true)    => State.Locked,
    (State.Closed, Operation.Close, _)      => throw new Exception(
        "Can't close a closed door"),
    _ => state
};

图 4 中的代码先构造新元组,其中包含当前状态、所需操作以及检查用户是否拥有有效密钥的布尔值。这是一个非常简单的场景。

根据这些不同的值,我可以通过构造更多的元组以及一个位置模式来匹配不同的情况。这就是元组模式。如果我尝试打开关着但没有锁着的门,就会产生新的状态,告知门现在是开着的。如果门是锁着的,并且我尝试用无用的钥匙来开门,门就会继续锁着。如果我尝试打开开着的门,我就会收到一个异常。您明白了吧ЎЈ这是一种非常灵活且有趣的方法,可用于处理前文中代码非常冗长且畸形冗长导致可读性过差的情况。

结语

C# 8.0 中的模式匹配改进,以及 switch 表达式,确实会改变开发人员编写应用程序的方式。C# 已有近 20 年的历史,它的演变过程反映了应用程序的生成方式。模式匹配仅仅是这一演变过程中最新的表达式。

对于开发人员而言,避免过度使用这些新原则和模式是明智之举。注意你所编写的代码,并确保可读性、可理解性和可维护性都良好。以上就是其他开发人员请求的内容,并且我认为对语言的这些更改将有助于提高你所编写代码的信噪比。

你可能感兴趣的:(C#)