C# 强大无匹的模式匹配,让代码更加优雅【代码之美系列】

🎀🎀🎀代码之美系列目录🎀🎀🎀

一、C# 命名规则规范
二、C# 代码约定规范
三、C# 参数类型约束
四、浅析 B/S 应用程序体系结构原则
五、浅析 C# Async 和 Await
六、浅析 ASP.NET Core SignalR 双工通信
七、浅析 ASP.NET Core 和 MongoDB 创建 Web API
八、浅析 ASP.NET Web UI 框架 Razor Pages/MVC/Web API/Blazor
九、如何使用 MiniProfiler WebAPI 分析工具
十、浅析 .NET Core 中各种 Filter
十一、C#.Net筑基-类型系统
十二、C#.Net 筑基-运算符
十三、C#.Net筑基-解密委托与事件
十四、C#.Net筑基-集合知识大全
十五、C#.Net筑基 - 常见类型
十六、C#.NET体系图文概述—2024最全总结


前言

long long ago , 我们在处理复杂的业务逻辑时可能会写很多 if else 语句,看着堆叠如山的屎山代码自己都心虚。以前没办法,因为没有更强大的语法糖🍬做背书,巧妇难为无米之炊啊。自从C#匹配模式增强之后,我看了很多官方的案例,太巧妙了,太优雅了。今天就来一起看看有哪些优雅的模式。


一、简介

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

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

  • is 表达式
  • switch 语句
  • switch 表达式

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

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

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

二、声明和类型模式

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

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

类型为 的声明模式在表达式结果为非 null 且满足以下任一条件时与表达式匹配:

  • 表达式结果的运行时类型为 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> 接口。

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

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

下面的示例演示最后两个条件:

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

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

  • integerfloating-point 数值文本
  • 字符型
  • 字符串字面量。
  • 布尔值 truefalse
  • enum
  • 声明常量字段或本地的名称
  • null

表达式类型必须 可转换常量类型,但有一个例外:类型为 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",
};

在关系模式中,可使用关系运算符 <><=>= 中的任何一个。 关系模式的 右侧 部分必须是常数表达式。 常数表达式可以是 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 或未能通过可为空或取消装箱转换转换为常量类型,则 关系模式表达式 不匹配

有关详细信息,请参阅功能建议说明的关系模式部分。

五、逻辑模式

请使用 notandor 模式连结符来创建以下逻辑模式:

  • 否定 模式在否定模式与表达式不匹配时与表达式匹配 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 绑定到操作数。 以下示例尝试匹配不小写字母 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 } };

上一示例使用 or 模式结合法和 记录 类型。

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

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 方法中的参数顺序匹配。 这是因为为位置模式生成的代码调用 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 模式来匹配任何表达式(包括 ),并将其结果分配给新的局部变量,如以下示例所示:

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 语句中的模式。 在这些案例中,要匹配任何表达式,请使用带有弃元 varvar _ 。 弃元模式可以是表达式 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

有关详细信息,请参阅列表模式功能建议说明。

十四、总结

读完你会发下匹配模式真的很强大,合理运用可以减少代码体积,使代码更容易理解和阅读。主要是优雅,我喜欢优雅!

在这里插入图片描述

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Microi风闲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值