🎀🎀🎀代码之美系列目录🎀🎀🎀
一、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
和弃元。 可以使用布尔逻辑关键字 and
、or
和 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)),
};
在常量模式中,可使用 任何常量
表达式,例如:
integer
或floating-point
数值文本- 字符型
- 字符串字面量。
- 布尔值
true
或false
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",
};
在关系模式中,可使用关系运算符 <
、>
、<=
或 >=
中的任何一个。 关系模式的 右侧
部分必须是常数表达式。 常数表达式可以是 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
或未能通过可为空或取消装箱转换转换为常量类型,则 关系模式
与 表达式
不匹配 。
有关详细信息,请参阅功能建议说明的关系模式部分。
五、逻辑模式
请使用 not
、and
和 or
模式连结符来创建以下逻辑模式:
- 否定 模式在否定模式与表达式不匹配时与表达式匹配
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
语句中的模式。 在这些案例中,要匹配任何表达式,请使用带有弃元 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
有关详细信息,请参阅列表模式功能建议说明。
十四、总结
读完你会发下匹配模式真的很强大,合理运用可以减少代码体积,使代码更容易理解和阅读。主要是优雅,我喜欢优雅!