模式比對 -
使用 is 表達式、 switch 陳述式和 switch 表達式 來將輸入表達式與任意數量的特徵匹配。 C# 支援多個模式,包括宣告、類型、常數、關聯式、屬性、清單、var 和捨棄。 你可以透過使用布林邏輯關鍵字 and、 or、 和 not來組合模式。
C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。
文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。
提示
欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。
下列 C# 運算式和陳述式支援模式比對:
在這些建構中,您可以比對輸入運算式與下列任何模式:
- 宣告模式:檢查運行時表達式的類型,如果匹配成功,將表達式的結果指派給宣告的變數。
- 類型模式:檢查表達式的運行時間類型。
- 常數模式:測試表達式結果是否等於指定的常數。
- 關係型模式:比較表達式結果與指定的常數。
- 邏輯模式:測試表達式是否符合模式的邏輯組合。
- 屬性模式:測試表達式的屬性或字段是否符合巢狀模式。
- 位置模式:解構表達式結果,並測試產生的值是否符合巢狀模式。
-
varpattern:比對任何表達式,並將其結果指派給宣告的變數。 - 捨棄模式:比對任何表達式。
- 清單模式:測試專案序列是否符合對應的巢狀模式。
邏輯、屬性、位置和清單模式是「遞迴」模式。 即,它們可以包含「巢狀」模式。
關於如何利用這些模式建立資料驅動演算法的範例,請參見 教學:使用模式匹配來建立型別驅動與資料驅動演算法。
宣告和類型模式
使用宣告與型態模式來檢查表達式的執行時型別是否與特定型別相容。 使用宣告模式,您也可以宣告新的區域變數。 當宣告模式與表達式匹配時,變數會被指派給轉換後的表達式結果,如下範例所示:
object greeting = "Hello, World!";
if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: hello, world!
}
運算式結果為非 Null 且符合下列任何條件時,具有類型 的「宣告模式」T會比對運算式:
- 運算式結果的執行時間類型會將身分識別轉換成
T。 - 型態
T是一個ref struct型態,且從表達式轉換為T。 - 運算式結果的執行階段類型衍生自類型
T、實作介面T或從它到 的另一個T。 此條件涵蓋繼承關係與介面實作。 下列範例示範此條件為 true 時的兩個案例:
在上述範例中,第一次呼叫var numbers = new int[] { 10, 20, 30 }; Console.WriteLine(GetSourceLabel(numbers)); // output: 1 var letters = new List<char> { 'a', 'b', 'c', 'd' }; Console.WriteLine(GetSourceLabel(letters)); // output: 2 static int GetSourceLabel<T>(IEnumerable<T> source) => source switch { Array array => 1, ICollection<T> collection => 2, _ => 3, };GetSourceLabel方法時,第一個模式會比對引數值,因為引數的執行階段類型int[]衍生自 Array 類型。 第二次呼叫GetSourceLabel方法時,引數的執行階段類型 List<T> 不會衍生自 Array 類型,而是實作 ICollection<T> 介面。 - 表達式結果的執行時間類型是可空值型別,其基礎類型是
T,而Nullable<T>.HasValue是true。 - 當表達式不是的實例時,表達式結果的運行時間類型會存在boxing或
T轉換到類型ref struct。
宣告模式不會考慮使用者定義轉換或隱含範圍轉換。
下列範例示範最後兩個條件:
int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
Console.WriteLine(a + b); // output: 30
}
若要只檢查表達式的型別,請用 discard _ 代替變數名稱,如下範例所示:
public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}
public static class TollCalculator
{
public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
Car _ => 2.00m,
Truck _ => 7.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
}
為此,請使用 型別模式,如下範例所示:
public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
Car => 2.00m,
Truck => 7.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
如同宣告模式,當表達式結果為非 Null 且其運行時間類型滿足上述任何條件時,類型模式會比對表達式。
if (input is not null)
{
// ...
}
常數模式
常數模式是右作數為常數時的==替代語法。 使用 常數模式 來測試表達式結果是否等於指定的常數,如下範例所示:
public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
1 => 12.0m,
2 => 20.0m,
3 => 27.0m,
4 => 32.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};
在常數模式中,您可以使用任何常數運算式,例如:
表達式必須是可轉換為常數型別的型別,但有一個例外:一個型別為 Span<char>ReadOnlySpan<char> 或 能與常數字串匹配的表達式。
使用常數模式來檢查 null,如下列範例所示:
if (input is null)
{
return;
}
編譯器保證在評估運算式 == 時,不會叫用任何使用者多載相等運算子 x is null。
您可以使用否定null 常數模式來檢查非 Null,如下列範例所示:
if (input is not null)
{
// ...
}
如需詳細資訊,請參閱功能提案附註的常數模式小節。
關聯式模式
使用 關聯模式 將表達式結果與常數進行比較,如下範例所示:
Console.WriteLine(Classify(13)); // output: Too high
Console.WriteLine(Classify(double.NaN)); // output: Unknown
Console.WriteLine(Classify(2.4)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};
在關聯式模式中,您可以使用任何關係運算子<、>、<= 或 >=。 關聯式模式的右側部分必須是常數運算式。 常數運算式可以是 integer、floating-point、char 或 enum 類型。
若要檢查運算式結果是否位於特定範圍,請將其與連結 and 模式進行比對,如下列範例所示:
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14))); // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // output: winter
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
如果運算式結果為 null,或無法透過可為 Null 或 unboxing 轉換來轉換成常數類型,則關聯式模式與運算式不符。
如需詳細資訊,請參閱功能提案附註的關聯式模式小節。
邏輯模式
使用 not、 and和 or 模式組合子來建立以下 邏輯模式:
否定
not模式在否定模式與運算式不相符時,與運算式相符。 下列範例顯示如何否定常數null模式,以檢查運算式是否為非 Null:if (input is not null) { // ... }連結
and模式在兩個模式都符合運算式時,與運算式相符。 下列範例顯示如何合併關聯式模式來檢查值是否位於特定範圍內:Console.WriteLine(Classify(13)); // output: High Console.WriteLine(Classify(-100)); // output: Too low Console.WriteLine(Classify(5.7)); // output: Acceptable static string Classify(double measurement) => measurement switch { < -40.0 => "Too low", >= -40.0 and < 0 => "Low", >= 0 and < 10.0 => "Acceptable", >= 10.0 and < 20.0 => "High", >= 20.0 => "Too high", double.NaN => "Unknown", };分隔
or模式在任一模式符合運算式時,與運算式相符,如下列範例所示:Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring static string GetCalendarSeason(DateTime date) => date.Month switch { 3 or 4 or 5 => "spring", 6 or 7 or 8 => "summer", 9 or 10 or 11 => "autumn", 12 or 1 or 2 => "winter", _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."), };
如上述範例所示,您可以在模式中重複使用模式結合器。
優先順序和檢查順序
模式組合子的順序如下,基於表達式的綁定順序:
notandor
模式會 not 先系結至其操作數。 模式 and 系結在任何模式表達式系結之後 not 。 畢竟,or模式會結合運算元,而and模式會結合運算not元。 以下範例嘗試匹配所有非小寫字母的字元。 a - z 發生錯誤,因為模式會在 not 模式之前 and 系結:
// Incorrect pattern. `not` binds before `and`
static bool IsNotLowerCaseLetter(char c) => c is not >= 'a' and <= 'z';
預設系結表示先前的範例會剖析為下列範例:
// The default binding without parentheses is shows in this method. `not` binds before `and`
static bool IsNotLowerCaseLetterDefaultBinding(char c) => c is ((not >= 'a') and <= 'z');
要修正錯誤,請指定你希望 not 模式綁定到表達式 >= 'a' and <= 'z' :
// Correct pattern. Force `and` before `not`
static bool IsNotLowerCaseLetterParentheses(char c) => c is not (>= 'a' and <= 'z');
當您的模式變得更複雜時,新增括弧會變得更加重要。 一般而言,使用括弧來釐清其他開發人員的模式,如下列範例所示:
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
注意
檢查具有相同系結順序之模式的順序未定義。 在運行時間,可以先檢查多個 or 模式和多個 and 模式的右側巢狀模式。
如需詳細資訊,請參閱功能提案附註的模式結合器小節。
屬性模式
使用 屬性模式 將表達式的屬性或欄位與巢狀模式匹配,如下範例所示:
static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
當表達式結果非空且每個巢狀模式都符合該表達式結果的相應屬性或欄位時,屬性模式即與表達式匹配。
你可以在屬性模式中加入執行時型別檢查和變數宣告,如下範例所示:
Console.WriteLine(TakeFive("Hello, world!")); // output: Hello
Console.WriteLine(TakeFive("Hi!")); // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' })); // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' })); // output: abc
static string TakeFive(object input) => input switch
{
string { Length: >= 5 } s => s.Substring(0, 5),
string s => s,
ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
ICollection<char> symbols => new string(symbols.ToArray()),
null => throw new ArgumentNullException(nameof(input)),
_ => throw new ArgumentException("Not supported input type."),
};
這個結構特別表示 空 屬性模式 is { } 符合所有非空的,你可以用它代替 is not null 建立變數: somethingPossiblyNull is { } somethingDefinitelyNotNull。
if (GetSomeNullableStringValue() is { } nonNullValue) // Empty property pattern with variable creation
{
Console.WriteLine("NotNull:" + nonNullValue);
}
else
{
nonNullValue = "NullFallback"; // we can access the variable here.
Console.WriteLine("it was null, here's the fallback: " + nonNullValue);
}
屬性模式是遞迴模式。 您可以使用任何模式作為巢狀模式。 使用屬性模式來比對資料各部分與巢狀模式,如下列範例所示:
public record Point(int X, int Y);
public record Segment(Point Start, Point End);
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start: { Y: 0 } } or { End: { Y: 0 } };
您可以引用屬性模式中的巢狀屬性或欄位。 此功能稱為「擴充屬性模式」。 例如,您可以將上述範例中的方法重構為下列對等程式碼:
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
如需詳細資訊,請參閱功能提案附註的屬性模式小節以及擴充屬性模式功能提案附註。
提示
為了提升程式碼可讀性,請使用 Simplify 屬性模式(IDE0170) 樣式規則。 它建議了使用擴展物業模式的地點。
位置模式
使用 位置模式 來拆解表達式,並將所得值與相應的巢狀模式匹配,如下範例所示:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
static string Classify(Point point) => point switch
{
(0, 0) => "Origin",
(1, 0) => "positive X basis end",
(0, 1) => "positive Y basis end",
_ => "Just a point",
};
在前述範例中,表達式的型別包含 解構 方法,該模式用來解構表達式結果。
重要
位置模式中的成員順序必須與 Deconstruct 方法中的參數順序相符。 為位置模式產生的程式碼呼叫該 Deconstruct 方法。
您也可以比對 Tuple 類型的運算式與位置模式。 透過此方法,你可以將多個輸入與不同模式匹配,如下範例所示:
static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)
=> (groupSize, visitDate.DayOfWeek) switch
{
(<= 0, _) => throw new ArgumentException("Group size must be positive."),
(_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
(>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
(>= 10, DayOfWeek.Monday) => 30.0m,
(>= 5 and < 10, _) => 12.0m,
(>= 10, _) => 15.0m,
_ => 0.0m,
};
您可以在位置模式中使用 Tuple 元素和 Deconstruct 參數的名稱,如下列範例所示:
var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}"); // output: Sum of [1 2 3] is 6
}
static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
int sum = 0;
int count = 0;
foreach (int number in numbers)
{
sum += number;
count++;
}
return (sum, count);
}
您也可以使用下列任何方式來擴充位置模式:
新增執行階段類型檢查和變數宣告,如下列範例所示:
public record Point2D(int X, int Y); public record Point3D(int X, int Y, int Z); static string PrintIfAllCoordinatesArePositive(object point) => point switch { Point2D (> 0, > 0) p => p.ToString(), Point3D (> 0, > 0, > 0) p => p.ToString(), _ => string.Empty, };在位置模式內使用屬性模式,如下列範例所示:
public record WeightedPoint(int X, int Y) { public double Weight { get; set; } } static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };合併上述兩種用法,如下列範例所示:
if (input is WeightedPoint (> 0, > 0) { Weight: > 0.0 } p) { // .. }
位置模式是遞迴模式。 即,您可以使用任何模式作為巢狀模式。
如需詳細資訊,請參閱功能提案附註的位置模式小節。
var 模式
使用 var 模式 匹配任意表達式,包括 null,並將結果指派到新的局部變數,如下範例所示:
static bool IsAcceptable(int id, int absLimit) =>
SimulateDataFetch(id) is var results
&& results.Min() >= -absLimit
&& results.Max() <= absLimit;
static int[] SimulateDataFetch(int id)
{
var rand = new Random();
return Enumerable
.Range(start: 0, count: 5)
.Select(s => rand.Next(minValue: -10, maxValue: 11))
.ToArray();
}
當您在布林運算式內需要暫存變數來保存中繼計算結果時,var 模式十分有用。 如果您需要在 var 運算式或陳述式的 when 案例防護中執行更多檢查,則也可以使用 switch 模式,如下列範例所示:
public record Point(int X, int Y);
static Point Transform(Point point) => point switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
static void TestTransform()
{
Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
}
在上述範例中,模式 var (x, y) 相當於位置模式(var x, var y)。
在模式 var 中,宣告變數的型別是該模式所匹配的運算式的編譯時型別。
如需詳細資訊,請參閱功能提案附註的 Var 模式小節。
捨棄模式
使用 棄置模式_ 來匹配任何表達式,包括 null,如下範例所示:
Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
DayOfWeek.Monday => 0.5m,
DayOfWeek.Tuesday => 12.5m,
DayOfWeek.Wednesday => 7.5m,
DayOfWeek.Thursday => 12.5m,
DayOfWeek.Friday => 5.0m,
DayOfWeek.Saturday => 2.5m,
DayOfWeek.Sunday => 2.0m,
_ => 0.0m,
};
在前述範例中,棄置模式處理 null 和 任何不包含該枚舉對應成員 DayOfWeek 的整數值。 這種保證確保 switch 範例中的表達式能處理所有可能的輸入值。 如果您未在 switch 運算式中使用捨棄模式,而且運算式模式不符合輸入,則執行階段會擲回例外狀況。 如果 switch 運算式未處理所有可能的輸入值,則編譯器會產生警告。
捨棄模式不能是 is 運算式或 switch 陳述式中的模式。 在這些情況下,若要比對任何運算式,請搭配使用 var 模式與捨棄:var _。 捨棄模式可以是 switch 運算式中的模式。
如需詳細資訊,請參閱功能提案附註的捨棄模式小節。
小括號內的模式
您可以使用括號來括住任何模式。 一般而言,您這麼做是要強調或變更邏輯模式中的優先順序,如下列範例所示:
if (input is not (float or double))
{
return;
}
清單模式
你可以將陣列或清單與一 連串 模式比對,如下範例所示:
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [1, 2, 3, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
如前例所示,當每個巢狀模式與輸入序列對應元素匹配時,列表模式即為匹配。 您可以在清單模式內使用任何模式。 要匹配任何元素,可以使用 棄牌模式 ,或者如果你也想捕捉該元素,則使用 var 模式,如下範例所示:
List<int> numbers = new() { 1, 2, 3 };
if (numbers is [var first, _, _])
{
Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.
上述範例會比對整個輸入序列與清單模式。 若只要比對輸入序列開頭或/和結尾的元素,請使用配量模式..,如下列範例所示:
Console.WriteLine(new[] { 1, 2, 3, 4, 5 } is [> 0, > 0, ..]); // True
Console.WriteLine(new[] { 1, 1 } is [_, _, ..]); // True
Console.WriteLine(new[] { 0, 1, 2, 3, 4 } is [> 0, > 0, ..]); // False
Console.WriteLine(new[] { 1 } is [1, 2, ..]); // False
Console.WriteLine(new[] { 1, 2, 3, 4 } is [.., > 0, > 0]); // True
Console.WriteLine(new[] { 2, 4 } is [.., > 0, 2, 4]); // False
Console.WriteLine(new[] { 2, 4 } is [.., 2, 4]); // True
Console.WriteLine(new[] { 1, 2, 3, 4 } is [>= 0, .., 2 or 4]); // True
Console.WriteLine(new[] { 1, 0, 0, 1 } is [1, 0, .., 0, 1]); // True
Console.WriteLine(new[] { 1, 0, 1 } is [1, 0, .., 0, 1]); // False
配量模式符合零或多個項目。 您最多可以在清單模式中使用一個配量模式。 配量模式只能出現在清單模式中。
您也可以在配量模式內將子模式巢狀處理,如下列範例所示:
void MatchMessage(string message)
{
var result = message is ['a' or 'A', .. var s, 'a' or 'A']
? $"Message {message} matches; inner part is {s}."
: $"Message {message} doesn't match.";
Console.WriteLine(result);
}
MatchMessage("aBBA"); // output: Message aBBA matches; inner part is BB.
MatchMessage("apron"); // output: Message apron doesn't match.
void Validate(int[] numbers)
{
var result = numbers is [< 0, .. { Length: 2 or 4 }, > 0] ? "valid" : "not valid";
Console.WriteLine(result);
}
Validate(new[] { -1, 0, 1 }); // output: not valid
Validate(new[] { -1, 0, 0, 1 }); // output: valid
如需詳細資訊,請參閱清單模式功能提案附註。
C# 語言規格
關於 C# 9 及後續版本新增的功能資訊,請參閱以下功能提案說明: