模式比對概觀

模式比對是測試運算式以判斷是否具有特定特性的技巧。 C# 模式比對提供更簡潔的語法,可測試運算式並在運算式相符時採取動作。 「is 運算式」支援透過模式比對來測試運算式,並有條件地將新的變數宣告至該運算式的結果。 「switch 運算式」可讓您根據運算式的最初相符模式來執行動作。 這兩個運算式支援豐富的模式詞彙。

本文提供可使用模式比對的案例概觀。 這些技巧可以改善程式碼的可讀性和正確性。 如需所有可套用模式的完整介紹,請參閱語言參考中有關模式的文章。

Null 檢查

模式比對最常見的案例之一是確保值不是 null。 您可以測試將可為 Null 的實值型別轉換成其基礎型別,同時使用下列範例測試 null

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

上述程式碼是一種宣告模式,用來測試變數的型別並指派給新的變數。 語言規則使這項技巧比許多其他技巧還要安全。 變數 number 只能在 if 子句的 true 部分中存取和指派。 如果嘗試在其他地方存取它,不管是在 else 子句中或 if 區塊後,編譯器都會發出錯誤。 其次,因為您不是使用 == 運算子,因此當型別多載 == 運算子時,此模式可運作。 如此一來便成為檢查 Null 參考值的理想方式,新增了 not 模式:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

上述範例使用常數模式來比較變數與 nullnot邏輯模式,會在否定模式不相符時相符。

型別測試

模式比對的另一個常見用法是測試變數,確認是否符合指定的型別。 例如,下列程式碼會測試變數是否非 Null,並實作 System.Collections.Generic.IList<T> 介面。 如果結果是肯定的,便會使用該清單上的 ICollection<T>.Count 屬性來尋找中間索引。 不論變數的編譯時間型別為何,宣告模式都不會比對 null 值。 下列程式碼除了防範未實作 IList 的型別之外,也會防範 null

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

switch 運算式中可以套用相同的測試,以針對多個不同型別測試變數。 您可以使用該資訊,根據特定的執行階段型別建立更好的演算法。

比較離散值

您也可以測試變數,以尋找特定值的相符項目。 下列程式碼顯示一個範例,您可針對列舉中宣告的所有可能值來測試值:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

上述範例會根據列舉的值示範方法分派。 最後一個 _ 案例是會比對所有值的捨棄模式。 它會處理值不符合其中一個定義 enum 值的任何錯誤條件。 如果您省略該 switch arm,編譯程式會警告您的模式表示式不會處理所有可能的輸入值。 在執行階段,如果所檢查的物件不符合任何 switch arm,switch 運算式就會擲回例外狀況。 您可以使用數值常數,而不是一組列舉值。 您也可以針對代表命令的常數位字串值使用這個類似的技巧:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

上述範例顯示相同的演算法,但會使用字串值,而不是列舉。 如果應用程式回應文字命令而不是一般資料格式,便可以使用此案例。 從 C# 11 開始,您也可以使用 Span<char>ReadOnlySpan<char> 來測試常數字串值,如下列範例所示:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

在這些範例中,捨棄模式可確保您處理每個輸入。 編譯器可協助您確保每個可能的輸入值都經過處理。

關聯式模式

您可以使用關聯式模式來測試值與常數的比較方式。 例如,下列程式碼會根據華氏溫度傳回水溫狀態:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

上述程式碼也會示範結合and邏輯模式,以檢查這兩種關聯式模式是否相符。 您也可以使用分離 or 模式來檢查任一模式是否相符。 這兩個關聯式模式會以括弧括住,您可以在任何模式周圍使用而不會混淆。 最後兩個 switch arm 處理熔點和沸點的案例。 如果沒有這兩個 arm,編譯器會警告您邏輯無法涵蓋每個可能的輸入。

上述程式碼也示範編譯器為模式比對運算式提供的另一個重要功能:如果沒有處理每個輸入值,編譯器會發出警告。 如果交換器臂的模式是由先前模式所涵蓋,編譯程式也會發出警告。 這可讓您自由重構和重新排序 switch 運算式。 撰寫相同運算式的另一種方式:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

上述範例中的關鍵課程,以及任何其他重構或重新排序,是編譯程式會驗證您的程式代碼是否處理所有可能的輸入。

多重輸入

到目前為止涵蓋的所有模式都已檢查一個輸入。 您可以撰寫模式來檢查物件的多個屬性。 請參考下列 Order 記錄:

public record Order(int Items, decimal Cost);

上述位置記錄型別會在明確位置宣告兩個成員。 先出現的是 Items,然後是訂單的 Cost。 如需詳細資訊,請參閱記錄

下列程式碼會檢查訂單的項目數和值,以計算折扣價格:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

前兩個 arm 會檢查 Order 的兩個屬性。 第三個檢查只會檢查成本。 接下來會針對 null 進行檢查,而最終會比對任何其他值。 如果 Order 型別定義了合適的 Deconstruct 方法,您可以在模式中省略屬性名稱,並使用解構來檢查屬性:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

上述程式碼示範解構運算式屬性的位置模式

清單模式

您可以使用清單模式來檢查清單或陣列中的元素。 清單模式可用來將模式套用至一個序列中的任何元素。 此外,您可以套用捨棄模式 (_) 來比對任何元素,或套用配量模式來比對零或多個元素。

當資料沒有遵循一般結構時,清單模式是很有價值的工具。 您可以使用模式比對來測試資料的圖形和值,而不是轉換成一組物件。

請思考下列摘錄自包含銀行交易資料的文字檔:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

這是 CSV 格式,但有些資料列的資料行比其他資料列多。 更糟的是處理,類型中的 WITHDRAWAL 一個數據行包含用戶產生的文字,而且可以在文字中包含逗號。 清單模式,其中包含捨棄模式、常數模式和 var 模式,以擷取值以下列格式處理資料:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

上述範例採用字串陣列,其中每個元素都是資料列中的一個欄位。 第二個欄位的 switch 運算式索引鍵,決定了交易種類,以及剩餘資料行的數量。 每個資料列都會確保資料的格式正確。 捨棄模式 (_) 會略過第一個欄位 (包含交易的日期)。 第二個欄位符合交易類別。 剩餘的元素相符項目會跳到包含數量的欄位。 最終比對會使用 var 模式來擷取數量的字串表示。 運算式會計算要從餘額加上或減去的金額。

清單模式可讓您比對資料元素序列的圖形。 您可以使用捨棄配量模式來比對元素的位置。 您可以使用其他模式來比對個別元素的特性。

本文提供導覽,說明您能使用模式比對以 C# 撰寫的程式碼類型。 下列文章顯示更多使用模式的案例範例,以及可用的模式完整詞彙。

另請參閱