• C#:模式匹配与模式


    模式匹配介绍


    C# 7 引入了基本模式匹配功能。 在 C# 8 至 C# 10 中,通过新增表达式和模式扩展了这些功能。 

    “模式匹配”是一种测试表达式是否具有特定特征的方法。 C# 模式匹配提供更简洁的语法,用于测试表达式并在表达式匹配时采取措施。

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

    模式


    声明和类型模式

    使用声明和类型模式检查表达式的运行时类型是否与给定类型兼容。

    借助声明模式,还可声明新的局部变量。

    当声明模式与表达式匹配时,将为该变量分配转换后的表达式结果,如以下示例所示:

    1. object greeting = "Hello, World!";
    2. if (greeting is string message)
    3. {
    4. Console.WriteLine(message.ToLower()); // output: hello, world!
    5. }

    从 C# 7.0 开始,类型为 T 的声明模式在表达式结果为非 NULL 且满足以下任一条件时与表达式匹配:

    • 表达式结果的运行时类型为 T

    • 表达式结果的运行时类型派生自类型 T,实现接口 T,或者存在从其到 T 的另一种隐式引用转换

    • 表达式结果的运行时类型是具有基础类型 T 的可为 null 的值类型

    • 存在从表达式结果的运行时类型到类型 T 的装箱 或取消装箱转换。

    常量模式

    从 C# 7.0 开始,可使用常量模式来测试表达式结果是否等于指定的常量,如以下示例所示:

    1. public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
    2. {
    3. 1 => 12.0m,
    4. 2 => 20.0m,
    5. 3 => 27.0m,
    6. 4 => 32.0m,
    7. 0 => 0.0m,
    8. _ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
    9. };

    在常量模式中,可使用任何常量表达式,例如:

    表达式类型必须可转换为常量类型,但有一个例外:类型为 Span 或 ReadOnlySpan 的表达式可以在 C# 11 及更高版本中针对常量字符串进行匹配。

    关系模式

    从 C# 9.0 开始,可使用关系模式将表达式结果与常量进行比较,如以下示例所示:

    1. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14))); // output: spring
    2. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // output: summer
    3. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // output: winter
    4. static string GetCalendarSeason(DateTime date) => date.Month switch
    5. {
    6. >= 3 and < 6 => "spring",
    7. >= 6 and < 9 => "summer",
    8. >= 9 and < 12 => "autumn",
    9. 12 or (>= 1 and < 3) => "winter",
    10. _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
    11. };

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

    如果表达式结果为 null 或未能通过可为空或取消装箱转换转换为常量类型,则关系模式与表达式不匹配。

    逻辑模式

    从 C# 9.0 开始,可使用 notand 和 or 模式连结符来创建以下逻辑模式:

    • 否定 not 模式在否定模式与表达式不匹配时与表达式匹配。 下面的示例说明如何否定常量null 模式来检查表达式是否为非空值:
    1. if (input is not null)
    2. {
    3. // ...
    4. }
    • 合取 and 模式在两个模式都与表达式匹配时与表达式匹配。 以下示例显示如何组合关系模式来检查值是否在某个范围内:
    1. Console.WriteLine(Classify(13)); // output: High
    2. Console.WriteLine(Classify(-100)); // output: Too low
    3. Console.WriteLine(Classify(5.7)); // output: Acceptable
    4. static string Classify(double measurement) => measurement switch
    5. {
    6. < -40.0 => "Too low",
    7. >= -40.0 and < 0 => "Low",
    8. >= 0 and < 10.0 => "Acceptable",
    9. >= 10.0 and < 20.0 => "High",
    10. >= 20.0 => "Too high",
    11. double.NaN => "Unknown",
    12. };
    • 析取 or 模式在任一模式与表达式匹配时与表达式匹配,如以下示例所示:
    1. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter
    2. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn
    3. Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring
    4. static string GetCalendarSeason(DateTime date) => date.Month switch
    5. {
    6. 3 or 4 or 5 => "spring",
    7. 6 or 7 or 8 => "summer",
    8. 9 or 10 or 11 => "autumn",
    9. 12 or 1 or 2 => "winter",
    10. _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
    11. };

    优先级

    以下列表按优先级从高到低的顺序对模式结合法进行排序:

    • not
    • and
    • or

    要显式指定优先级,请使用括号,如以下示例所示:

    static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

    属性模式

    从 C# 8.0 开始,可使用属性模式来将表达式的属性或字段与嵌套模式进行匹配,如以下示例所示:

    static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

    当表达式结果为非 NULL 且每个嵌套模式都与表达式结果的相应属性或字段匹配时,属性模式将与表达式匹配。

    还可将运行时类型检查和变量声明添加到属性模式,如以下示例所示:

    1. Console.WriteLine(TakeFive("Hello, world!")); // output: Hello
    2. Console.WriteLine(TakeFive("Hi!")); // output: Hi!
    3. Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' })); // output: 12345
    4. Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' })); // output: abc
    5. static string TakeFive(object input) => input switch
    6. {
    7. string { Length: >= 5 } s => s.Substring(0, 5),
    8. string s => s,
    9. ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    10. ICollection<char> symbols => new string(symbols.ToArray()),
    11. null => throw new ArgumentNullException(nameof(input)),
    12. _ => throw new ArgumentException("Not supported input type."),
    13. };

    位置模式

    从 C# 8.0 开始,可使用位置模式来解构表达式结果并将结果值与相应的嵌套模式匹配,如以下示例所示:

    1. public readonly struct Point
    2. {
    3. public int X { get; }
    4. public int Y { get; }
    5. public Point(int x, int y) => (X, Y) = (x, y);
    6. public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    7. }
    8. static string Classify(Point point) => point switch
    9. {
    10. (0, 0) => "Origin",
    11. (1, 0) => "positive X basis end",
    12. (0, 1) => "positive Y basis end",
    13. _ => "Just a point",
    14. };

    var 模式

    从 C# 7.0 开始,可使用 var 模式来匹配任何表达式(包括 null),并将其结果分配给新的局部变量,如以下示例所示:

    1. static bool IsAcceptable(int id, int absLimit) =>
    2. SimulateDataFetch(id) is var results
    3. && results.Min() >= -absLimit
    4. && results.Max() <= absLimit;
    5. static int[] SimulateDataFetch(int id)
    6. {
    7. var rand = new Random();
    8. return Enumerable
    9. .Range(start: 0, count: 5)
    10. .Select(s => rand.Next(minValue: -10, maxValue: 11))
    11. .ToArray();
    12. }

    需要布尔表达式中的临时变量来保存中间计算的结果时,var 模式很有用。 当需要在 switch 表达式或语句的 when 大小写临界子句中执行其他检查时,也可使用 var 模式,如以下示例所示:

    1. public record Point(int X, int Y);
    2. static Point Transform(Point point) => point switch
    3. {
    4. var (x, y) when x < y => new Point(-x, y),
    5. var (x, y) when x > y => new Point(x, -y),
    6. var (x, y) => new Point(x, y),
    7. };
    8. static void TestTransform()
    9. {
    10. Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
    11. Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
    12. }

    在前面的示例中,模式 var (x, y) 等效于位置模式(var x, var y)

    在 var 模式中,声明变量的类型是与该模式匹配的表达式的编译时类型。

    弃元模式

    从 C# 8.0 开始,可使用弃元模式 _ 来匹配任何表达式,包括 null,如以下示例所示:

    1. Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
    2. Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
    3. Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
    4. static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
    5. {
    6. DayOfWeek.Monday => 0.5m,
    7. DayOfWeek.Tuesday => 12.5m,
    8. DayOfWeek.Wednesday => 7.5m,
    9. DayOfWeek.Thursday => 12.5m,
    10. DayOfWeek.Friday => 5.0m,
    11. DayOfWeek.Saturday => 2.5m,
    12. DayOfWeek.Sunday => 2.0m,
    13. _ => 0.0m,
    14. };

    在前面的示例中,弃元模式用于处理 null 以及没有相应的 DayOfWeek 枚举成员的任何整数值。 这可保证示例中的 switch 表达式可处理所有可能的输入值。 如果没有在 switch 表达式中使用弃元模式,并且该表达式的任何模式均与输入不匹配,则运行时会引发异常。 如果 switch 表达式未处理所有可能的输入值,则编译器会生成警告。

    弃元模式不能是 is 表达式或 switch 语句中的模式。 在这些案例中,要匹配任何表达式,请使用带有弃元 var _ 的 var 模式

    带括号模式

    从 C# 9.0 开始,可在任何模式两边加上括号。 通常,这样做是为了强调或更改逻辑模式中的优先级,如以下示例所示:

    1. if (input is not (float or double))
    2. {
    3. return;
    4. }

    列表模式

    从 C# 11 开始,可以将数组或列表与匹配元素的模式序列相匹配。 可以应用以下任一模式:

    • 任何模式都可以应用于任何元素,以检查单个元素是否与某些特征匹配。
    • 弃元模式 (_) 与单个元素匹配。
    • 范围模式 (..) 可以匹配序列中的零个或多个元素。 列表模式中最多允许有一个范围模式。
    • var 模式可以捕获单个元素或一系列元素。

    这些规则通过以下数组声明进行了演示:

    1. int[] one = { 1 };
    2. int[] odd = { 1, 3, 5 };
    3. int[] even = { 2, 4, 6 };
    4. int[] fib = { 1, 1, 2, 3, 5 };

    可以通过指定所有元素并使用值来匹配整个序列:

    1. Console.WriteLine(odd is [1, 3, 5]); // true
    2. Console.WriteLine(even is [1, 3, 5]); // false (values)
    3. Console.WriteLine(one is [1, 3, 5]); // false (length)

    可以使用弃元模式 (_) 作为占位符,在一个已知长度的序列中匹配一些元素:

    1. Console.WriteLine(odd is [1, _, _]); // true
    2. Console.WriteLine(odd is [_, 3, _]); // true
    3. Console.WriteLine(even is [_, _, 5]); // false (last value)

    可以在序列中的任何位置提供任意数量的值或占位符。 如果不关心长度,可以使用范围模式来匹配零个或多个元素:

    1. Console.WriteLine(odd is [1, .., 3, _]); // true
    2. Console.WriteLine(fib is [1, .., 3, _]); // true
    3. Console.WriteLine(odd is [1, _, 5, ..]); // true
    4. Console.WriteLine(fib is [1, _, 5, ..]); // false

    前面的示例使用了常量模式来确定某个元素是否是给定的数字。 这些模式中的任何一种都可以替换为其他模式,例如关系模式:

    1. Console.WriteLine(odd is [_, >1, ..]); // true
    2. Console.WriteLine(even is [_, >1, ..]); // true
    3. Console.WriteLine(fib is [_, > 1, ..]); // false

    当数据不遵循常规结构时,列表模式是一个有价值的工具。 可以使用模式匹配来测试数据的形状和值,而不是将其转换为一组对象。

    看看下面的内容,它摘录自一个包含银行交易信息的文本文件:

    1. 04-01-2020, DEPOSIT, Initial deposit, 2250.00
    2. 04-15-2020, DEPOSIT, Refund, 125.65
    3. 04-18-2020, DEPOSIT, Paycheck, 825.65
    4. 04-22-2020, WITHDRAWAL, Debit, Groceries, 255.73
    5. 05-01-2020, WITHDRAWAL, #1102, Rent, apt, 2100.00
    6. 05-02-2020, INTEREST, 0.65
    7. 05-07-2020, WITHDRAWAL, Debit, Movies, 12.57
    8. 04-15-2020, FEE, 5.55

    它是 CSV 格式,但某些行的列数比其他行要多。 对处理来说更糟糕的是,WITHDRAWAL 类型中的一列具有用户生成的文本,并且可以在文本中包含逗号。 一个包含弃元模式、常量模式和 var 模式的列表模式用于捕获这种格式的值处理数据:

    1. decimal balance = 0m;
    2. foreach (var transaction in ReadRecords())
    3. {
    4. balance += transaction switch
    5. {
    6. [_, "DEPOSIT", _, var amount] => decimal.Parse(amount),
    7. [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
    8. [_, "INTEREST", var amount] => decimal.Parse(amount),
    9. [_, "FEE", var fee] => -decimal.Parse(fee),
    10. _ => throw new InvalidOperationException($"Record {transaction} is not in the expected format!"),
    11. };
    12. Console.WriteLine($"Record: {transaction}, New balance: {balance:C}");
    13. }

    前面的示例采用了字符串数组,其中每个元素都是行中的一个字段。 第二个字段的开关表达式键,用于确定交易的类型和剩余列数。 每一行都确保数据的格式正确。 弃元模式 (_) 跳过第一个字段,以及交易的日期。 第二个字段与交易的类型匹配。 其余元素匹配跳过包含金额的字段。 最终匹配使用 var 模式来捕获金额的字符串表示形式。 表达式计算要从余额中加上或减去的金额。

    列表模式可以在数据元素序列的形状上进行匹配。 使用弃元 模式和范围模式来匹配元素的位置。 使用其他模式来匹配各个元素的特征。

    参考:

            Microsoft C# 模式匹配

            Microsoft C# 模式

            Microsoft C# 教程

            Microsoft C# 递归模式匹配

  • 相关阅读:
    mybatis-传递参数的方式
    初始 JDBC
    lua脚本的基础内容
    云e办(后端)——根据id查询菜单
    UNIAPP-ADB无线调试
    P1090 [NOIP2004 提高组] 合并果子【堆、贪心】
    牛客多校10 - Yet Another FFT Problem?(鸽巢原理)
    【第2期赠书活动】〖Python 数据库开发实战 - Redis篇⑤〗- Redis 的常用配置参数
    vivado使用方法(初级)
    Java面试八股文之暑假合集
  • 原文地址:https://blog.csdn.net/m0_38037668/article/details/125162526