模式比對 - is
和 switch
運算式、運算子 and
、 or
、 和 not
模式中的
您可以使用表達式、switch 語句和 switch 運算式來比對輸入表達式與任意數目的特性。is
C# 支援多個模式,包括宣告、類型、常數、關聯式、屬性、清單、var 和捨棄。 您可以使用布林邏輯關鍵字 and
、or
和 not
來合併模式。
下列 C# 運算式和陳述式支援模式比對:
在這些建構中,您可以比對輸入運算式與下列任何模式:
- 宣告模式:檢查運算式的執行階段類型,而且,如果比對成功,則請將運算式結果指派給已宣告變數。
- 類型模式:檢查運算式的執行階段類型。
- 常數模式:測試表達式結果是否等於指定的常數。
- 關聯式模式:比較運算式結果與指定的常數。
- 邏輯模式:測試表達式是否符合模式的邏輯組合。
- 屬性模式:測試表達式的屬性或字段是否符合巢狀模式。
- 位置模式:解構運算式結果,並測試產生的值是否符合巢狀模式。
var
模式:比對任何運算式,並將其結果指派給已宣告的變數。- 捨棄模式:比對任何運算式。
- 清單模式:測試專案序列是否符合對應的巢狀模式。 已在 C# 11 中引進。
邏輯、屬性、位置和清單模式是「遞迴」模式。 即,它們可以包含「巢狀」模式。
如需如何使用這些模式來建置資料驅動演算法的範例,請參閱教學課程:使用模式比對來建置類型驅動和資料驅動演算法。
宣告和類型模式
您可以使用宣告和類型模式來檢查運算式的執行階段類型是否與給定類型相容。 使用宣告模式,您也可以宣告新的區域變數。 宣告模式符合運算式時,會將已轉換的運算式結果指派給該變數,如下列範例所示:
object greeting = "Hello, World!";
if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: hello, world!
}
運算式結果為非 Null 且符合下列任何條件時,具有類型 T
的「宣告模式」會比對運算式:
運算式結果的執行階段類型為
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
的可為 Null 實值型別。
下列範例示範最後兩個條件:
int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
Console.WriteLine(a + b); // output: 30
}
如果您只想要檢查運算式的類型,則可以使用捨棄 _
來取代變數的名稱,如下列範例所示:
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 且其運行時間類型滿足上述任何條件時,類型模式會比對表達式。
若要檢查非 Null,您可以使用否定 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>
的運算式可以與 C# 11 和更新版本中的常數字串進行比對。
使用常數模式來檢查 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}."), };
如上述範例所示,您可以在模式中重複使用模式結合器。
優先順序和檢查順序
模式結合子會根據表達式的系結順序來排序,如下所示:
not
and
or
模式會 not
先系結至其操作數。 模式 and
系結在任何模式表達式系結之後 not
。 模式 or
會在所有 not
之後系結,而且 and
模式會系結至操作數。 下列範例會嘗試比對不小寫字母的所有字元。 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 };
運算式結果為非 Null 且每個巢狀模式都符合運算式結果的對應屬性或欄位時,屬性模式會比對運算式。
您也可以將執行階段類型檢查和變數宣告新增至屬性模式,如下列範例所示:
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."),
};
屬性模式是遞迴模式。 即,您可以使用任何模式作為巢狀模式。 使用屬性模式來比對資料各部分與巢狀模式,如下列範例所示:
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 } };
從 C# 10 開始,您可以參考屬性模式內的巢狀屬性或欄位。 此功能稱為「擴充屬性模式」。 例如,您可以將上述範例中的方法重構為下列對等程式碼:
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
如需詳細資訊,請參閱功能提案附註的屬性模式小節以及擴充屬性模式功能提案附註。
提示
您可以使用簡化屬性模式 (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, };
上述範例使用可隱含提供
Deconstruct
方法的位置記錄。在位置模式內使用屬性模式,如下列範例所示:
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
模式十分有用。 如果您需要在 switch
運算式或陳述式的 when
案例防護中執行更多檢查,則也可以使用 var
模式,如下列範例所示:
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;
}
清單模式
從 C# 11 開始,您可以比對陣列或清單與「一連串」模式,如下列範例所示:
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# 8 和更新版本中新增功能的詳細資訊,請參閱下列功能提案附注: