教程:使用 C# 将数据与模式匹配

本教程介绍如何使用模式匹配来检查 C# 中的数据。 你将编写少量的代码,然后编译并运行这些代码。 本教程包含一系列课程,探讨 C# 支持的不同类型的模式。 这些课程介绍了 C# 语言的基础知识。

在本教程中,你将:

  • 使用 C# 开发环境启动 GitHub Codespace。
  • 测试离散值的数据。
  • 将枚举数据与值匹配。
  • 使用 switch 表达式创建全面匹配。
  • 使用类型模式来匹配类型。

先决条件

您必须拥有以下之一:

匹配一个值

前面的教程展示了内置类型和您定义为元组或记录的类型。 可以根据 模式检查这些类型的实例。 实例是否与模式匹配,决定了你的程序采取的操作。 在下面的示例中,你将在类型名称后看到 ? 。 此符号允许此类型的值为 null(例如, bool? 可以是 truefalsenull)。 有关详细信息,请参阅 可以为 Null 的值类型。 让我们开始探讨如何使用模式。

打开 GitHub codespaces 的浏览器窗口。 从 .NET 模板创建新的代码空间。 如果已完成本系列中的其他教程,则可以打开该代码空间。

  1. 在代码空间加载时,在名为 patterns.cs教程文件夹中创建新文件。

  2. 打开新文件。

  3. 本教程中的所有示例都使用文本输入,该输入将一系列银行交易表示为逗号分隔值(CSV)输入。 在每个示例中,可以使用is表达式或switch表达式将记录与模式匹配。 第一个示例使用,字符拆分每一行,然后使用表达式将第一个字符串字段与值“DEPOSIT”或“WITHDRAWAL”匹配。 匹配时,将从当前帐户余额中添加或扣除交易金额。 若要查看它是否正常工作,请将以下代码添加到 patterns.cs

    string bankRecords = """
        DEPOSIT,   10000, Initial balance
        DEPOSIT,     500, regular deposit
        WITHDRAWAL, 1000, rent
        DEPOSIT,    2000, freelance payment
        WITHDRAWAL,  300, groceries
        DEPOSIT,     700, gift from friend
        WITHDRAWAL,  150, utility bill
        DEPOSIT,    1200, tax refund
        WITHDRAWAL,  500, car maintenance
        DEPOSIT,     400, cashback reward
        WITHDRAWAL,  250, dining out
        DEPOSIT,    3000, bonus payment
        WITHDRAWAL,  800, loan repayment
        DEPOSIT,     600, stock dividends
        WITHDRAWAL,  100, subscription fee
        DEPOSIT,    1500, side hustle income
        WITHDRAWAL,  200, fuel expenses
        DEPOSIT,     900, refund from store
        WITHDRAWAL,  350, shopping
        DEPOSIT,    2500, project milestone payment
        WITHDRAWAL,  400, entertainment
        """;
    
    double currentBalance = 0.0;
    var reader = new StringReader(bankRecords);
    
    string? line;
    while ((line = reader.ReadLine()) is not null)
    {
        if (string.IsNullOrWhiteSpace(line)) continue;
        // Split the line based on comma delimiter and trim each part
        string[] parts = line.Split(',');
    
        string? transactionType = parts[0]?.Trim();
        if (double.TryParse(parts[1].Trim(), out double amount))
        {
            // Update the balance based on transaction type
            if (transactionType?.ToUpper() is "DEPOSIT")
                currentBalance += amount;
            else if (transactionType?.ToUpper() is "WITHDRAWAL")
                currentBalance -= amount;
    
            Console.WriteLine($"{line.Trim()} => Parsed Amount: {amount}, New Balance: {currentBalance}");
        }
    }
    
  4. 然后,在终端窗口中键入以下文本:

    cd tutorials
    dotnet patterns.cs
    
  5. 检查输出。 通过比较第一个字段中的文本值,可以看到每一行都得到处理。

可以类似地使用==运算符来测试两值是否相等,并据此构造前面的示例。 将变量与常量进行比较是用于模式匹配的基本构建基块。 让我们进一步探索模式匹配中的构建模块。

枚举匹配项

模式匹配的另一个常见用途是对某个enum类型的值进行匹配。 以下示例处理输入记录以创建 元组 ,其中第一个 enum 值是记录存款或取款的值。 第二个值是交易的值。

  1. 将以下代码添加到源文件的末尾。 它定义 TransactionType 枚举:

    public enum TransactionType
    {
        Deposit,
        Withdrawal,
        Invalid
    }
    
  2. 添加一个函数,将银行事务分析为保存事务类型和事务值的元组。 在枚举声明 TransactionType 之前添加以下代码:

    static IEnumerable<(TransactionType type, double amount)> TransactionRecords(string inputText)
    {
        var reader = new StringReader(inputText);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            string[] parts = line.Split(',');
    
            string? transactionType = parts[0]?.Trim();
            if (double.TryParse(parts[1].Trim(), out double amount))
            {
                // Update the balance based on transaction type
                if (transactionType?.ToUpper() is "DEPOSIT")
                    yield return (TransactionType.Deposit, amount);
                else if (transactionType?.ToUpper() is "WITHDRAWAL")
                    yield return (TransactionType.Withdrawal, amount);
            }
            else {
            yield return (TransactionType.Invalid, 0.0);
            }
        }
    }
    
  3. 在使用你声明的枚举 TransactionType 后,添加一个新循环来处理交易数据。

    currentBalance = 0.0;
    
    foreach (var transaction in TransactionRecords(bankRecords))
    {
        if (transaction.type == TransactionType.Deposit)
            currentBalance += transaction.amount;
        else if (transaction.type == TransactionType.Withdrawal)
            currentBalance -= transaction.amount;
        Console.WriteLine($"{transaction.type} => Parsed Amount: {transaction.amount}, New Balance: {currentBalance}");
    }
    

前面的示例还使用语句 if 检查表达式的值 enum 。 另一种模式匹配形式使用 switch 表达式。 让我们探讨一下该语法以及如何使用它。

包含的详尽匹配项 switch

一系列 if 语句可以测试一系列条件。 但是,编译器无法判断一系列 if 语句是否 详尽 ,或者后续 if 条件是否被早期条件 子化详尽 意味着一系列测试中的某个 ifelse 子句处理所有可能的输入。 如果一系列 if 语句详尽无遗,则每个可能的输入至少满足一个 ifelse 子句。 Subsumption 表示后面的ifelse子句无法被访问,因为之前的ifelse子句已经匹配了所有可能的输入。 例如,在以下示例代码中,一个子句永远不会匹配:

int n = GetNumber();

if (n < 20)
    Console.WriteLine("n is less than 20");
else if (n < 10)
    Console.WriteLine("n is less than 10"); // unreachable
else
    Console.WriteLine("n is greater than 20");

子句else if无法匹配,因为任何小于10的数字肯定小于20。 该 switch 表达式可确保满足这两个特征,这会导致应用中的 bug 更少。 让我们来尝试和实验。

  1. 复制以下代码。 将循环中的ifforeach个语句替换为复制的switch表达式。

    currentBalance += transaction switch
    {
        (TransactionType.Deposit, var amount) => amount,
        (TransactionType.Withdrawal, var amount) => -amount,
        _ => 0.0,
    };
    
  2. 在终端窗口中键入 dotnet patterns.cs 以运行新示例。

    运行代码时,会看到它的工作方式相同。

  3. 若要演示 子建议,请重新排序开关臂,如以下代码片段所示:

    currentBalance += transaction switch
    {
        (TransactionType.Deposit, var amount) => amount,
        _ => 0.0,
        (TransactionType.Withdrawal, var amount) => -amount,
    };
    

    重新排序开关臂后,在终端窗口中键入 dotnet patterns.cs 。 编译器发出错误,因为 arm 与 _ 每个值匹配。 因此,最终的分支永远不会 TransactionType.Withdrawal 运行。 编译器会告诉你代码中存在错误。

    如果在switch表达式中被测试的表达式包含不匹配任何switch分支的值,编译器将发出警告。 如果某些值可能无法匹配任何条件,则 switch 表达式并不 详尽。 如果输入的某些值与任何开关臂不匹配,编译器也会发出警告。

  4. 删除包含_ => 0.0,的这一行,以确保任何无效值不匹配。

  5. 键入 dotnet patterns.cs 看看结果如何。

    编译器发出警告。 测试数据有效,因此程序有效。 但是,任何无效数据都会在运行时导致故障。

类型模式

若要完成本教程,请浏览一个用于模式匹配的构建块:类型模式类型模式在运行时测试表达式,以查看它是否为指定类型。 可以将类型测试与is表达式或switch表达式之一一起使用。 通过两种方式修改当前示例。 首先,生成 DepositWithdrawal 作为表示事务的记录类型,而不是元组。

  1. 在代码文件末尾添加以下声明:

    public record Deposit(double Amount, string description);
    public record Withdrawal(double Amount, string description);
    
  2. 在枚举声明 TransactionType 之前添加此方法。 它分析文本并返回一系列记录:

    static IEnumerable<object?> TransactionRecordType(string inputText)
    {
        var reader = new StringReader(inputText);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            string[] parts = line.Split(',');
    
            string? transactionType = parts[0]?.Trim();
            if (double.TryParse(parts[1].Trim(), out double amount))
            {
                // Update the balance based on transaction type
                if (transactionType?.ToUpper() is "DEPOSIT")
                    yield return new Deposit(amount, parts[2]);
                else if (transactionType?.ToUpper() is "WITHDRAWAL")
                    yield return new Withdrawal(amount, parts[2]);
            }
            yield return default;
        }
    }
    
  3. 在最后 foreach 一个循环后添加以下代码:

    currentBalance = 0.0;
    
    foreach (var transaction in TransactionRecordType(bankRecords))
    {
        currentBalance += transaction switch
        {
            Deposit d => d.Amount,
            Withdrawal w => -w.Amount,
            _ => 0.0,
        };
        Console.WriteLine($" {transaction} => New Balance: {currentBalance}");
    }
    
  4. 在终端窗口中键入 dotnet patterns.cs 以查看结果。 此最终版本针对 类型测试输入。

模式匹配提供了用于比较表达式与特征的词汇。 模式可以包括表达式的类型、类型的值、属性值以及它们的组合。 将表达式与模式进行比较可能比多个 if 比较更清晰。 你浏览了一些可用于匹配表达式的模式。 在应用程序中使用模式匹配的方法更多。 浏览时,可以在以下文章中详细了解 C# 中的模式匹配:

清理资源

GitHub 会在 30 天不活动后自动删除 Codespace。 你已完成本系列中的所有教程。 若要立即删除 Codespace,请打开浏览器窗口并转到 Codespaces。 应该会在窗口中看到代码空间的列表。 在学习教程代码空间的条目中选择三个点(...),然后选择 “删除”。