我第一次接触模式匹配是在几年前,当时我正在摆弄一些函数式编程语言,比如 Elixir 和 F#。当我用 C#(以及 JavaScript)编写代码时,我缺少对值或对象进行模式匹配的能力。
我很高兴看到 C# 7 引入了 模式匹配的第一个版本,它支持最低限度的功能。由于我之前接触过模式匹配,所以这有点令人失望,因为它没有我习惯的所有可能性。幸运的是,C# 8 的发布通过支持更多功能扩展了模式匹配语法,这些功能在 C# 9 中得到了进一步增强,而且这种趋势似乎在即将推出的 C# 版本中仍在继续。
为什么要进行模式匹配
那么我为什么对此感到兴奋呢?
使用模式匹配可以做的所有事情,在不使用模式匹配的情况下也可以做到,但看起来不太好。
模式匹配可以将复杂的 if-else 或 switch 语句变成紧凑的代码块。
因此,随着模式匹配的增加,我的开发体验得到了改善。
它的优势在于它具有表现力,使其更易于阅读,更难编写错误。
额外的好处是编译器充当安全网(稍后会详细介绍),它会警告未处理的情况并防止情况相互冲突。
这不是普通 if-else 语句所能提供的功能,不幸的是,它过去曾困扰过我。
很高兴知道编译器会报告愚蠢的错误来支持你。
对于本文的其余部分,让我们看一些如何使用模式匹配和 switch 表达式的示例。
一个简单的例子
最简单的例子是检查简单的真/假值。
模式匹配将输入值(下例中为 false
)与一组定义的模式进行比较,将它们视为一组条件。 switch 表达式中的模式从上到下进行评估,并执行导致匹配的第一个案例。
var output = false switch
{
true => "true",
false => "false",
};
// output: false
常量模式
我们可以使用模式匹配来检查变量是否具有常量值。
可匹配的类型是字符串、字符、数字和枚举。
var output = 4 switch
{
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
};
// output: four
丢弃模式
由于上例中的 switch 表达式中并非包含所有数字,因此 C# 编译器会警告存在未涵盖的情况。
warning CS8509: The switch expression does not handle all possible values
of its input type (it is not exhaustive).
For example, the pattern '0' is not covered.
为了解决此警告,我们可以添加一个后备案例。
将其视为 switch 语句中的默认案例,即当先前的模式均不匹配时调用的通配符。在下面的示例中,丢弃运算符(表示为下划线“_”)用于匹配所有其他可能的值。
var output = 9 switch
{
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
_ => "other"
};
// output: other
变量模式
匹配的值可以分配给变量。
在下面的例子中,变量充当与任何值匹配的通配符。
当 类型模式 的输入值与类型匹配时,也可以定义变量,在这种情况下,创建的变量是类型化的。然后可以在执行表达式(=>
之后)中使用该变量来创建返回值。
此外,可以将**when
语句添加到模式中,为模式添加额外的保护**。
when
语句可用于将非常量值与条件匹配,例如,在匹配的变量上调用方法。除了匹配正在匹配的对象之外,还可以在 when
语句中包含其他对象。
var greetWithName = true;
var output = "Mrs. Kim" switch
{
_ when greetWithName == false => $"Hi",
"Tim" => "Hi Tim!",
var str when str.StartsWith("Mrs.") || str.StartsWith("Mr.") => $"Greetings {str}",
var str => $"Hello {str}",
};
// output: Greetings Mrs. Kim
关系模式
我们使用关系运算符(=
、>
、<
、>=
、<=
)来测试输入是否等于、大于或小于另一个值。
var output = 4 switch
{
< 3 => "less than 3",
<= 7 => "less than or equal to 7",
< 1 => "less than 10",
_ => "greater than or equal to 10"
};
// output: ?
你发现示例中的错误了吗?
如果没有,你不必担心,因为这次编译器会抛出一个错误来解决我的错误。
error CS8510: The pattern is unreachable. It has already been handled by
a previous arm of the switch expression or it is impossible to match.
让我们通过修正模式来修复这个错误。
var output = 4 switch
{
< 3 => "less than 3",
<= 7 => "less than or equal to 7",
< 10 => "less than 10",
_ => "greater than or equal to 10"
};
// output: less than or equal to 7
多种模式
为了组合模式或否定值,我们使用逻辑运算符(and
、or
、not
)。
var output = 4 switch
{
1 or 2 or 3 => "1, 2, or 3",
> 3 and <= 6 => "between 3 and 6",
not 7 => "not 7",
_ => "7"
};
// output: between 3 and 6
元组模式
匹配单个值很好,但在很多情况下用处不大。
要匹配多个值,我们可以使用元组对多个输入值进行模式匹配。
var output = (5, false) switch
{
(< 4, true) => "lower than 4 and true",
(< 4, false) => "lower than 4 and false",
(4, true) => "4 and true",
(5, _) => "five and something",
(_, false) => "something and false",
_ => "something else",
};
// output: five and something
属性模式
当输入是对象时,我们可以向对象的属性添加模式。
var output = new User("Tim Deschryver", "Developer") switch
{
{ Role: "Admin" } => "the user is an admin",
{ Role: "Administrator" } => "the user is an admin",
{ Name: "Tim", Role: "Developer" } => "the user is Tim and he is a developer",
{ Name: "Tim" } => "the user is Tim and he isn't a developer",
_ => "the user is unknown",
};
// output: the user is Tim and he is a developer
record User(string Name, string Role);
嵌套属性模式
甚至可以匹配嵌套属性。
var output = new Member("Tim Deschryver", new MemberDetails(8, false)) switch
{
{ Details: { Blocked: true } } => Array.Empty<string>(),
{ Details: { MonthsSubscribed: < 3 } } => new[] { "comments" },
{ Details: { MonthsSubscribed: < 9 } } => new[] { "comments", "mention" },
_ => new[] { "comments", "mention", "ping" },
};
// output: comments, mention
record Member(string Name, MemberDetails Details);
record MemberDetails(int MonthsSubscribed, bool Blocked);
上述示例可能难以阅读(尤其是当您需要深入了解多个对象时)。
作为补救措施,C# 10 提供了一种称为 扩展属性模式 的新语法,使其更容易阅读。
重构后的示例如下所示。好多了,对吧?
var output = new Member("Tim Deschryver", new MemberDetails(8, false)) switch
{
{ Details.Blocked: true } => Array.Empty<string>(),
{ Details.MonthsSubscribed: < 3 } => new[] { "comments" },
{ Details.MonthsSubscribed: < 9 } => new[] { "comments", "mention" },
_ => new[] { "comments", "mention", "ping" },
};
// output: comments, mention
record Member(string Name, MemberDetails Details);
record MemberDetails(int MonthsSubscribed, bool Blocked);
类型模式
模式匹配也可用于匹配对象的类型。
当您有一个充当传递的通用处理程序时,类型模式很有用。
var output = new InventoryItemRemoved(3) as object switch
{
// the variable `added` is of type `InventoryItemAdded`
InventoryItemAdded added => $"Added {added.Amount}",
// the variable `removed` is of type `InventoryItemRemoved`
InventoryItemRemoved removed => $"Removed {removed.Amount}",
InventoryItemDeactivated => "Deactivated",
null => throw new ArgumentNullException()
// the variable `o` is of type `object`
var o => throw new InvalidOperationException($"Unknown {o.GetType().Name}")
};
// output: Removed 3
示例
到目前为止我们看到的示例都很简单,它们只是展示了构建模式的不同语法。
为了释放模式匹配的真正威力,可以组合多种模式匹配策略。
// retrieve the rate of an appointment
var holidays = new DateTime[] {...};
var output = new Appointment(DayOfWeek.Friday, new DateTime(2021, 09, 10, 22, 15, 0), false) switch
{
{ SocialRate: true } => 5,
{ Day: DayOfWeek.Sunday } => 25,
Appointment a when holidays.Contains(a.Time) => 25,
{ Day: DayOfWeek.Saturday } => 20,
{ Day: DayOfWeek.Friday, Time.Hour: > 12 } => 20,
{ Time.Hour: < 8 or >= 18 } => 15,
_ => 10,
};
// output: 20
record Appointment(DayOfWeek Day, DateTime Time, bool SocialRate);
// a combination of the variable pattern and the tuple pattern
var output = ("", "Tim") switch
{
var (title, name) when title.Equals("Mrs.") || title.Equals("Mr.") => $"Greetings {title} {name}",
var (_, name) and (_, "Tim") => $"Hi {name}!",
var (_, name) => $"Hello {name}",
};
// output: Hi Tim!
// format a string
var contactInfo = new ContactInfo("Sarah", "Peeters", "0123456789");
var output = contactInfo switch
{
{ TelephoneNumber: not null } and { TelephoneNumber: not "" } info => $"{info.FirstName} {info.LastName} ({info.TelephoneNumber})",
_ => $"{contactInfo.FirstName} {contactInfo.LastName}"
};
// output: Sarah Peeters (0123456789)
// C# Language Highlights: Tuple Pattern Matching https://www.youtube.com/watch?v=v_xKLwTv3AI
IEnumerable<string> sequence = new[] { "foo" };
var output = sequence switch
{
string[] { Length: 0 } => "array with no items",
string[] { Length: 1 } => "array with a single item",
string[] { Length: 2 } => "array with 2 items",
string[] => $"array with more than 2items",
IEnumerable<string> source when !source.Any() => "empty enumerable",
IEnumerable<string> source when source.Count() < 3 => "a small enumerable",
IList<string> list => $"a list with {list.Count} items",
null => "null",
_ => "something else"
};
// output: array with a single item
官方文档
我们在这篇文章中只看到了模式匹配表达式的示例。
有关更多详细信息,请查看官方文档: