教程:模式匹配基础知识

Tip

开发软件的新手? 首先开始 学习入门 教程。 它们引入了类、方法和控制流。

是否在其他语言中有经验? 本教程演示当规则依赖于对象的当前状态时,C# 模式如何清楚地表达对象行为。

在本教程中,你将生成一个控制台应用,用于为运河锁的规则建模。

运河闸在不同高度的两段水域之间升高或降低船只。 它有两个闸门和一个腔室,其水位在低设置和高设置之间变化。 锁只能在水位和门位保持有效组合时安全运行。

本教程中,您将学习如何:

  • 通过状态匹配来表达对象行为。
  • 使用 C# 模式匹配实现这些规则。
  • 使用编译器诊断来验证实现。

Prerequisites

安装说明

在 Windows 上,可以使用这个 WinGet 配置文件来安装所有必备组件。 如果已安装某些内容,WinGet 将跳过此步骤。

  1. 下载该文件,然后双击以运行它。
  2. 阅读许可协议,键入 y,并在系统提示接受时按下 Enter。
  3. 如果在任务栏中收到闪烁的用户帐户控制(UAC)提示,请允许安装继续。

在其他平台上,需要单独安装其中每个组件。

  1. 从 .NET SDK 下载页下载建议的安装程序然后双击以运行它。 下载页可检测平台,并推荐平台的最新安装程序。
  2. 从 Visual Studio Code 主页下载最新的安装程序,然后双击以运行它。 该页还会检测平台,并且链接应该适合你的系统。
  3. 单击 C# DevKit 扩展页上的“安装”按钮。 这将打开 Visual Studio 代码,并询问是否要安装或启用扩展。 选择“安装”。

生成对运河闸的模拟

运河闸在不同水位的水道之间升高和降低船只。 在本教程中,模拟船闸有下门、上门和水位,水位可高可低。

在正常操作中,当锁内的水位与入口端的水位匹配时,船进入。 一旦船只进入内部,两个闸门都会关闭。 水位变化以匹配出口侧,然后出口闸门打开。 为了保证模型安全,水位只能在两个门关闭时更改,并且仅当水位与该侧匹配时,门才能打开。

可以使用类 CanalLock 为这些规则建模。 它提供用于打开或关闭任一闸门,并升高或降低水位的命令。 它还公开报告锁的当前状态的属性。

定义类

创建控制台项目,然后添加一个名为 CanalLock 的类。 首先,设计公共 API 并保留未实现的方法:

public enum WaterLevel
{
    Low,
    High
}
public class CanalLock
{
    // Query canal lock state:
    public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
    public bool HighWaterGateOpen { get; private set; } = false;
    public bool LowWaterGateOpen { get; private set; } = false;

    // Change the upper gate.
    public void SetHighGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change the lower gate.
    public void SetLowGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change water level.
    public void SetWaterLevel(WaterLevel newLevel)
    {
        throw new NotImplementedException();
    }

    public override string ToString() =>
        $"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
        $"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
        $"The water level is {CanalLockWaterLevel}.";
}

前面的代码将两个闸门关闭且水位低的情况下初始化闸。 接下来,将以下代码添加到 Main 中,以指导你的第一个实现:

// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:
Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

现在,添加第一个实现,在不强制执行安全规则的情况下更改每个状态值:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = open;
}

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = open;
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = newLevel;
}

这些初步检查通过。 你已使机制正常工作。 接下来,为第一个失败条件添加测试。 在上一个序列的末尾,两个闸门都关闭,水位较低。 尝试打开上方闸门:

Console.WriteLine("=============================================");
Console.WriteLine("     Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
    canalGate = new CanalLock();
    canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

该测试失败,因为上方闸门在不应打开时打开。 第一个修复可能如下所示:

// Change the upper gate.
public void SetHighGate(bool open)
{
    if (open && (CanalLockWaterLevel == WaterLevel.High))
        HighWaterGateOpen = true;
    else if (open && (CanalLockWaterLevel == WaterLevel.Low))
        throw new InvalidOperationException("Cannot open high gate when the water is low");
}

你的测试再次通过了。 但是,当你添加更多条件时,你将累积更多的 if 语句。 代码更难浏览,因为每个规则都与其他规则分开。

使用模式实现命令

更清晰的选项是使用 模式集 直接描述有效组合。 在下一步中,每个开关表达式使用一个元组作为 模式输入。 C# 计算该元组一次,每个 switch 臂测试当前闸门状态、水位和请求的新设置。

对于上闸门,可以汇总这些组合,如下所示:

新设置 门状态 水位 Result
已关闭 已关闭 高 已关闭
已关闭 已关闭 低 已关闭
已关闭 Open 高 已关闭
Closed 打开 Low Closed
Open 已关闭 高 Open
Open 已关闭 低 已关闭(错误)
Open Open 高 Open
打开 打开 Low 已关闭(错误)

带删除线的行表示无效的内部状态。 switch 表达式可以直接对有效转换进行编码。 false 仍表示门已关闭:

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
    (false, false, WaterLevel.High) => false,
    (false, false, WaterLevel.Low) => false,
    (false, true, WaterLevel.High) => false,
    (false, true, WaterLevel.Low) => false, // should never happen
    (true, false, WaterLevel.High) => true,
    (true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
    (true, true, WaterLevel.High) => true,
    (true, true, WaterLevel.Low) => false, // should never happen
};

请尝试此版本。 您的测试已通过。 编译器还会警告说,switch 表达式并不 详尽(它并不涵盖每个可能的值),因为 WaterLevel 是一个枚举,而 C# 允许将该枚举的基础数值类型的任何值强制转换为该枚举,即使该值没有对应的命名成员。 添加具有放弃模式(_)的最终臂来处理这些不可能的内部状态:

_ => throw new InvalidOperationException("Invalid internal state"),

该臂必须位于最后,因为弃元模式匹配每个剩余输入。

然后你可以简化前面的臂。 关闭闸门始终被允许,因此一个臂可以替换四个单独的关闭情况:

(false, _, _) => false,

你还可以组合有效的打开情况并保留一个安全错误:

(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

再次运行程序。 测试仍然通过。 下面是最终 SetHighGate 实现:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _,    _)               => false,
        (true, _,     WaterLevel.High) => true,
        (true, false, WaterLevel.Low)  => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _                              => throw new InvalidOperationException("Invalid internal state"),
    };
}

实现剩余规则

现在,将相同的思路应用到 SetWaterLevel 和 SetLowGate。 首先添加暴露无效操作问题的测试:

Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetLowGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetHighGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

再次运行应用。 这些测试失败,并且运河闸达到无效状态。 通过匹配完整状态来实现其余方法。 SetLowGate 类似于 SetHighGate。 SetWaterLevel 使用当前水位加上两个闸门位置:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // arms go here
};

有 16 个组合需要考虑。 从完整表开始,编写 switch 分支,运行测试,然后简化重复结果。

你最终有类似这些方法吗?

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _, _) => false,
        (true, _, WaterLevel.Low) => true,
        (true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open low gate when the water is high"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
    {
        (WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
        (WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
        (WaterLevel.Low, _, false, false) => WaterLevel.Low,
        (WaterLevel.High, _, false, false) => WaterLevel.High,
        (WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
        (WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

你的测试现在应该通过,并且运河闸应该强制执行其安全规则。

总结

在本教程中,你使用了模式匹配来表达对象如何从一个有效状态更改为另一个状态。 模式将允许的转换集中在一起,因此你可以比使用一系列长分支语句更轻松地对其进行比较。 当行为取决于多个状态值的组合形状时,此方法非常有效。