模式匹配 - 模式中的 isswitch 表达式,以及 andornot 运算符

is使用表达式switch 语句switch 表达式将输入表达式与任意数量的特征匹配。 C# 支持多种模式,包括声明、类型、常量、关系、属性、列表、var 和弃元。 可以使用布尔逻辑关键字< a0/> 组合模式。

C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。

本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。

提示

若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。

以下 C# 表达式和语句支持模式匹配:

在这些构造中,可将输入表达式与以下任一模式进行匹配:

  • 声明模式:检查表达式的运行时类型,如果匹配成功,请将表达式结果分配给声明的变量。
  • 类型模式:检查表达式的运行时类型。
  • 常量模式:测试表达式结果是否等于指定的常量。
  • 关系模式:将表达式结果与指定的常量进行比较。
  • 逻辑模式:测试表达式是否与模式的逻辑组合匹配。
  • 属性模式:测试表达式的属性或字段是否与嵌套模式匹配。
  • 位置模式:解构表达式结果并测试结果是否与嵌套模式匹配。
  • var 模式:匹配任何表达式并将其结果分配给声明的变量。
  • 丢弃模式:匹配任何表达式。
  • 列表模式:测试元素序列是否与相应的嵌套模式匹配。

逻辑属性位置列表模式都是递归模式。 也就是说,它们可包含嵌套模式

有关如何使用这些模式生成数据驱动算法的示例,请参阅 教程:使用模式匹配来生成类型驱动算法和数据驱动算法

声明和类型模式

使用声明和类型模式检查表达式的运行时类型是否与给定类型兼容。 通过使用声明模式,还可以声明新的局部变量。 当声明模式与表达式匹配时,它将变量分配给转换后的表达式结果,如以下示例所示:

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。 此条件涵盖继承关系和接口实现。 下面的示例演示满足此条件时的两种案例:
    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> 接口。
  • 表达式结果的运行时类型是一个可空值类型,其基础类型为 ,而TNullable<T>.HasValue
  • 当表达式不是 的实例时,从表达式结果的运行时类型到类型 存在Tref 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
}

若要仅检查表达式的类型,请使用放弃 _ 代替变量的名称,如以下示例所示:

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)
{
    // ...
}

有关详细信息,请参阅 C# 语言规范的 声明模式类型模式 部分。

常量模式

在右操作数为常量时,常量模式== 的替代语法。 使用 常量模式 测试表达式结果是否等于指定的常量,如以下示例所示:

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",
};

在关系模式中,使用任何关系运算符<><=>=。 关系模式的右侧部分必须是常数表达式。 常数表达式可以是 integerfloating-pointcharenum 类型。

要检查表达式结果是否在某个范围内,请将其与合取 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 或取消装箱转换转换为常量的类型,则关系模式与表达式不匹配。

有关详细信息,请参阅 C# 语言规范的关系 模式 部分。

逻辑模式

not使用和andor模式组合器创建以下逻辑模式

  • 否定not模式在否定模式与表达式不匹配时与表达式匹配。 下面的示例说明如何否定常量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 模式绑定到作数。 下面的示例尝试通过 匹配不小写字母 az的所有字符。 出现错误,因为 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 模式的右侧嵌套模式。

有关详细信息,请参阅 C# 语言规范的 模式组合器 部分。

属性模式

使用 属性模式 将表达式的属性或字段与嵌套模式匹配,如以下示例所示:

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."),
};

此构造特别意味着属性模式与所有非 null 属性模式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 } };

上一示例使用 or模式连结符记录类型

可以在属性模式中引用嵌套属性或字段。 该功能称为“扩展属性模式”。 例如,可将上述示例中的方法重构为以下等效代码:

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start.Y: 0 } or { End.Y: 0 };

有关详细信息,请参阅 C# 标准的属性 模式 部分。

提示

若要提高代码可读性,请使用 “简化”属性模式(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 该方法。

还可将元组类型的表达式与位置模式进行匹配。 通过使用此方法,可以将多个输入与各种模式匹配,如以下示例所示:

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,
    };

上一示例使用关系逻辑模式。

可在位置模式中使用元组元素的名称和 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 模式很有用。 当需要在 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# 15 开始,当其臂处理该类的每个直接后代时, switch 其治理类型为 closed 类的表达式 是详尽 的。 编译器不需要默认 arm,因为开关详尽:

public closed record class PaymentMethod;
public record class Cash : PaymentMethod;
public record class Card(string Last4) : PaymentMethod;
public record class BankTransfer(string Iban) : PaymentMethod;
public static string Describe(PaymentMethod method) => method switch
{
    Cash => "cash",
    Card(var last4) => $"card ending {last4}",
    BankTransfer(var iban) => $"bank transfer to {iban}",
    // No warning: every direct descendant of 'PaymentMethod' is handled.
};

仅当每个直接后代都可以从交换机的位置访问时,关闭的层次结构开关才详尽无遗。 如果直接后代的可访问性低于封闭基类型且在交换机站点不可见,编译器将它视为未经处理的,并警告该开关并不详尽。

例如,关闭 public 的基类可以具有 internal 直接后代。 同一程序集中的代码将看到完整的后代集,但另一个程序集中的代码不会:

// Assembly 1
public closed record class Shape;
public record class Circle(double Radius) : Shape;
internal record class Triangle(double Base, double Height) : Shape;
// Same assembly: 'Triangle' is visible, so the switch is exhaustive.
internal static double Area(Shape shape) => shape switch
{
    Circle(var r) => Math.PI * r * r,
    Triangle(var b, var h) => 0.5 * b * h,
};
// Assembly 2
public static string Name(Shape shape) => shape switch
{
    Circle => "circle",
    // Warning CS8509: the switch expression doesn't handle all possible values.
    // 'Triangle' is a direct descendant of 'Shape' but isn't visible here.
};

若要在程序集 2 中还原详尽性,请添加放弃臂(_ => ...)、添加基类臂(Shape 在前面的示例中),或使每个直接后代至少与封闭基类型一样易于访问。

当控制类型可为 null 时, null 开关必须处理的额外值。 即使匹配每个直接后代,省略PaymentMethod?手臂的开关null也不详尽。

从封闭类派生不是可传递的:封闭类的非封闭后代可以从其他程序集中派生。 编译器仅将 直接 后代视为详尽集。 若要使后代的切换也受益于详尽检查,声明后代 closed (或 sealed)。

由于间接后代不会扩展封闭基础的详尽集,因此无需为每个可传递子类型添加一个臂来满足详尽性。 臂之间的子句仍然的工作方式与任何类层次结构的工作方式相同:与基类型匹配的臂涵盖每个子类型,并且与其中一个子类型匹配的后续臂是无法访问的。 考虑其直接后代是封闭Vehicle的,以及Car在另一个程序集中声明的间接后代TruckSedan

// Assembly 1
public closed record class Vehicle;
public record class Car(int Doors) : Vehicle;
public record class Truck(double PayloadTons) : Vehicle;

// Assembly 2
public record class Sedan(int Doors) : Car(Doors);

切换Vehicle在处理CarTruck之后是详尽的,即使Sedan存在也是如此。 臂 Car 涵盖每个 Sedan 值:

public static string Category(Vehicle v) => v switch
{
    Car => "car",
    Truck => "truck",
    // No warning. The 'Car' arm covers 'Sedan' through ordinary subtype matching.
};

要特别调度Sedan,请将手臂放在手臂前面Car。 手臂Car仍然可访问,因为它仍然匹配每个Car不是:Sedan

public static string CategorySedanFirst(Vehicle v) => v switch
{
    Sedan => "sedan",
    Car => "car",      // Reachable: 'Car' values that aren't 'Sedan'.
    Truck => "truck",
};

反转这两个臂会产生子建议错误,就像它在任何其他类层次结构中一样。 编译器检测到 Car arm 已覆盖 Sedan

string Category(Vehicle v) => v switch
{
    Car => "car",
    Sedan => "sedan",  // Error CS8510: the pattern is unreachable. It has already been handled by 'Car => ...'.
    Truck => "truck",
};

如果希望更深入地遵循层次结构,请声明 Car 自身 closed。 然后,编译器将 (例如Car) 的每个直接后代Sedan视为根Car根于的详尽集。 当它执行以下任一操作时,其治理类型 Car 是详尽的开关:

  • 用自己的手臂处理每个直接后代 Car
  • 包括一个 Car 臂,它涵盖每个 Car 值(包括所有子类型)。
  • 包括放弃臂(_ => ...)或基类型的 Car手臂,例如 Vehicle

一个开关,其治理类型 Vehicle 有选择:处理 CarTruck 仍然详尽的代码,因为 Car 臂涵盖每个子类型 Car。 标记 Carclosed 只是为你提供了该开关的第二个选项。 你可以保留单 Car 臂,或将其替换为每个直接后代的 Car 一只手臂(与手臂一起 Truck ),仍然详尽无遗。

标记 Carclosed 也使它隐 abstract式化,这意味着你不能再直接创建实例 Car 。 该条件可能不适合你的设计。 如果需要 Car 保持可实例化,请将其保持打开状态,并通过订购武器来调度你关心的特定子类型,如前所述。

类型参数控制类型

封闭层次结构功能规范定义一个表达式,该表达式的治理类型是一个switch类型参数,该表达式约束为封闭类,其术语与切换关闭类本身时一样详尽。 因此,泛型代码可以通过将类型参数限制为封闭基,并处理每个直接后代,从而在封闭层次结构上调度:

public static string Describe<X>(X method) where X : PaymentMethod => method switch
{
    Cash => "cash",
    Card(var last4) => $"card ending {last4}",
    BankTransfer(var iban) => $"bank transfer to {iban}",
    // No warning: 'X' is constrained to the closed type 'PaymentMethod', so the
    // compiler treats this switch as exhaustive because every direct descendant is handled.
};

有关详细信息,请参阅 关闭的修饰符。 有关规范,请参阅 封闭层次结构

联合模式

从 C# 15 开始,当模式的传入值为 联合类型时,模式通常会 解包 联合。 该模式适用于联合 Value 的属性,而不是联合值本身。 此行为使联合对模式匹配透明:

public record class Cat(string Name);
public record class Dog(string Name);
public union Pet(Cat, Dog);

string Describe(Pet pet) => pet switch
{
    Dog d => d.Name,
    Cat c => c.Name,
};

三种模式是例外:放弃 _ 模式、 var 模式和 not 模式应用于联合值本身,而不是其 Value 属性。

模式 null 检查联合 Value 是否为 null。 对于基于类的联合,当联合引用本身为 null 时, null 也会成功。

当联合类型提供 非装箱访问模式时,编译器会调用 TryGetValue 类型模式检查和 HasValue null 模式检查,避免对值类型事例进行装箱。 每个成员都适用于其自身的模式类型 ,这两者都不是另一个成员的回退。 缺少成员时,编译器会改为检查 Value 该模式的属性。

有关详细信息,请参阅 联合匹配。 有关规范,请参阅 联合

C# 语言规范

有关详细信息,请参阅 C# 语言规范模式和模式匹配部分,包括:

另请参阅