模式比對概觀
模式比對是測試運算式以判斷是否具有特定特性的技巧。 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);
}
上述範例使用常數模式來比較變數與 null
。 not
是邏輯模式,會在否定模式不相符時相符。
型別測試
模式比對的另一個常見用法是測試變數,確認是否符合指定的型別。 例如,下列程式碼會測試變數是否非 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 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# 撰寫的程式碼類型。 下列文章顯示更多使用模式的案例範例,以及可用的模式完整詞彙。