关键要点
- C# 8 新增了 Ranges 和递归模式。
- 可以使用 Ranges 来定义数据序列,可用于替代 Enumberable.Range()。
- 递归模式为 C#带来了类似 F#的结构。
- 递归模式是一个非常棒的功能,为我们提供了一种灵活的方式,基于一系列条件来测试数据,并根据满足的条件执行进一步的计算。
- Ranges 可用于生成集合或列表形式的数字序列。
2015 年 1 月 21 日是 C#历史上最重要的日子之一。在这一天,C#专家 Anders Hejlsberg 和 Mads Torgersen 等人聚在一起畅谈 C#的未来,并思考了这门语言应该往哪个方向发展。
这次会议的第一个结果是 C# 7。第七个版本增加了一些新特性,并将重点放在数据消费、代码简化和性能上。针对 C# 8 的新提议并未改变对特性的关注,但在最终版本中可能会有所改变。
图 1. C# 7 和 8 的关注点
在本文中,我将讨论为 C# 8 提议的两个新特性。第一个是 Ranges,第二个是递归模式,它们都属于代码简化类别。我将通过很多示例详细地解释它们,我将向你展示这些特性如何帮助你写出更好的代码。
Ranges 可用于定义数据序列。它是 Enumerable.Range() 的替代品,只是它定义的是起点和终点,而不是起点和计数,它可以帮助你写出可读性更高的代码。
示例
foreach(var item in 1..100) { Console.WriteLine(item); }
递归模式匹配是一个非常强大的功能,主要与递归一起使用,可用它写出更加优雅的代码。 RecursivePatterns 包含多个子模式,例如位置模式(Positional Pattern,var isBassam = user is Employee(“Bassam”,_))、属性模式(Property Patterns,p is Employee {Name is “Mais”})、变量模式(Var Pattern)、丢弃模式(Discard Pattern,'_'),等等。
示例
带元组的递归模式(下面的例子也称为元组模式)
var employee = (Name: "Thomas Albrecht", Age: 43); switch (employee) { case (_, 43) employeeTmp when(employeeTmp.Name == "Thomas Albrecht "): { Console.WriteLine($ "Hi {employeeTmp.Name} you are now 43!"); } break; // 如果 employee 包含了其他信息,那么就执行下面的代码。 case _: Console.WriteLine("any other person!"); break; }
case (_,43) 可以解释如下:首先,“_”表示忽略 Name 属性,但 Age 必须为 43。如果 employee 元组包含 (任何字符串,43),则将执行 case 块。
尝试在这里运行上面的代码。
图 2. 递归模式的基本示例
我们过去曾在多篇文章中讨论过这个主题,但这是我们第一次深入研究模式匹配。
Ranges
这个特性是关于提供两个新的操作符(索引操作符“^”和范围操作符“..”),可以用它们来构造 System.Index 和 System.Range 对象,并使用它们在运行时对集合进行索引或切片。新的操作符其实是语法糖,让你的代码更加简洁。操作符索引 ^ 的代码使用 System.Index 实现,在范围操作符“..”使用 System.Range 实现。
System.Index
从结尾处对集合进行索引的绝佳方式。
示例
var lastItem = array[^1]; 与 var lastItem = array[collection.Count-1]; 是等效的。
System.Range
这是一种访问集合的“范围”或“切片”的方式。这样可以避免使用 LINQ,并让代码更加紧凑,可读性更高。你可以将它与 F#中的 Ranges 进行比较。
|
|
|
|
|
|
|
|
示例
考虑下面的数组:
var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Value |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
我们可以使用以下索引访问数组的值:
Index |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
现在,我们从这个数组中剪切出一个切片视图,如下所示:
var slice= array[2..5];
我们可以使用以下索引访问切片的值:
注意:起始索引是被包含在切片中的,而结束索引是不包含在切片中的。
var slice1 = array [4..^2]; // Range.Create(4, new Index(2, true))
slice1 的类型为 Span<int>。[4..^2] 从开始跳到索引 4,并从结尾跳过 2 个位置。
Output: 4, 5, 6, 7, 8 var slice2 = array [..^3]; // Range.ToEnd(new Index(3, true)) Output: 0, 1, 2, 3, 4, 5, 6, 7 var slice3 = array [2..]; // Range.FromStart(2) Output: 2, 3, 4, 5, 6, 7, 8,9, 10 var slice4 = array[..]; // array[Range.All] Output: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
可以在这里运行代码示例。
有边界 Ranges
在有边界 Ranges 中,下限(起始索引)和上限(结束索引)是已知的或预定义的。
array[start..end] // 获取从 start-1 到 end-1 的项 array[start..end:step] // 按照指定步长获取从 start-1 到 end-1 的项
上面的 Range 语法(后面跟上步长)源自 Python。Python 支持这样的语法(lower:upper:step),其中:step 是可选的,默认为 1,但社区中有一些人希望使用 F#的语法(lower..step..upper)。
你可以在此处跟进讨论: Range 操作符。
F#中的 Range 语法。
array { 5 .. 2 .. 20 } // 这里 2 = step [start .. step .. end]
输出:
5 7 9 11 13 15 17 19
有界 Range 示例
var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; var subarray = array[3..5]; // 选择的项为: 3, 4
上面的代码等同于 array.ToList().GetRange(3,2);。如果将 array.ToList().GetRange(3,2); 和 array[3..5] 进行对比,可以看出新的风格更清晰,更具人性化。
有一个功能请求是在“if”语句中使用 Range,或者使用如下所述的模式匹配:
使用“in”操作符
var anyChar = 'b'; if (anyChar in 'a'..'z') { Console.WriteLine($"The letter {anyChar} in range!"); } Output: The letter b in range!
Range 模式是新出现的模式匹配,可用于生成简单范围检查。在使用 Range 模式时,可在 switch 语句中使用 Range 操作符“..”。
switch (anyChar) { case in 'a'..'z' => Console.WriteLine($“The letter {anyChar} in range!”), case in '!'..'+' => Console.WriteLine($“Something else!”), } Output: The letter b in range!
值得一提的是,并非所有人都喜欢在 Ranges 中使用“in”操作符。社区中有人使用“in”,有人使用“is”,你可以在这里跟进整个讨论: C# Range 的问题。
无边界 Ranges
当省略下限时,默认为零,而当上限被省略时,默认为集合的长度。
示例
array[start..] // 获取从 start-1 开始的所有项 array[..end] // 获取从头开始到 end-1 的项 array[..] // 获取真个数组
正边界
var fiveToEnd = 5..; // 等同于 Range.From(5),也即缺失上界 var startToTen = ..1; // 等同于 Range.ToEnd(1),也即缺失下届,结果为: 0, 1 var everything = ..; // 等同于 Range.All,也即缺失上届和下届,结果为: 0..Int.Max var everything = [5..11]; // 等同于 Range.Create(5, 11) var collection = new [] { 'a', 'b', 'c' }; collection[2..]; // 输出: c collection[..2]; // 输出: a, b collection[..]; // 输出: a, b, c
负边界
你可以使用负边界。它们表示相对于集合的长度,1 表示最后一个元素,2 表示倒数第二个元素,依此类推。
示例
var collection = new [] { 'a', 'b', 'c' }; collection[-2..2]; // 结果: b, c collection[-1..]; // 结果: c collection[-3..-1]; // 结果: a, b
注意:目前,负面界限无法测试,如下所示:
图 3. 使用负索引导致的参数异常
Ranges 与字符串
可以使用索引来创建子字符串:
示例
var helloWorldStr = "Hello, World!"; var hello = helloWorldStr[..5]; Console.WriteLine(hello); // Output: Hello var world = helloWorldStr[7..]; Console.WriteLine(world); // Output: World
或者可以这样写:
var world = helloWorldStr[^6..]; // 获取最后 6 个字符 Console.WriteLine(world); // Output: World
Ranges 的 ForEach 循环
示例
使用 Ranges 来实现 IEnumerable<int>,可以对数据序列进行迭代。
foreach (var i in 0..10) { Console.WriteLine(“number {i}”); }
递归模式
模式匹配是一种功能强大的结构,出现在很多函数式编程语言中,如 F#。此外,模式匹配提供了解构匹配对象的能力,让你可以访问其数据结构的各个部分。C#为此提供了一组丰富的模式。
模式匹配最初计划出现在 C# 7 中,但后来.Net 团队发现他们需要更多时间来完成这个特性。因此,他们将这个任务分为两个部分。基本模式匹配已经在 C# 7 可用,而高级匹配模式则放在了 C# 8 中。我们已经在 C# 7 中看到了常量模式(Const Pattern)、类型模式(Type Pattern)、变量模式(Var Pattern)和丢弃模式(Discard Pattern)。在 C# 8 中,我们将看到更多的模式,如递归模式,它由多个子模式组成,如位置模式和属性模式。
要理解递归模式,需要很多示例代码。我已经定义了两个类。下面定义的 Employee 和 Company,我将用它们来解释递归模式。
public class Employee { public string Name { get; set; } public int Age { get; set; } public Company Company { get; set; } public void Deconstruct(out string name, out int age, out Company company) { name = Name; age = Age; company = Company; } } public class Company { public string Name { get; set; } public string Website { get; set; } public string HeadOfficeAddress { get; set; } public void Deconstruct(out string name, out string website, out string headOfficeAddress) { name = Name; website = Website; headOfficeAddress = HeadOfficeAddress; } }
位置模式
位置模式对匹配的类型进行分解,并基于返回的值执行进一步的模式匹配。这个模式的最终值为 true 或 false,决定了是否要执行后续的代码块。
if (employee is Employee(_, _, ("Stratec", _, _)) employeeTmp) { Console.WriteLine($ "The employee: {employeeTmp.Name}!"); } Output The employee: Bassam Alugili
在这个例子中,我递归地使用了模式匹配。第一部分是位置模式 employee is Employee(…),第二部分是括号内的子模式 (_,_, (“Stratec”,_,_))。
if 语句之后的代码块只在位置模式(employee 对象必须是 Employee 类型)中的条件及其子模式 (_,_,(“Stratec”,_,_))(即 company 名称必须是“Stratec”)都满足时才会执行,其余部分被丢弃。
属性模式
属性模式很直接了当,你可以访问类型字段和属性,并对它们应用进一步的模式匹配。
if (bassam is Employee {Name: "Bassam Alugili", Age: 42}) { Console.WriteLine($ "The employee: {bassam.Name} , Age {bassam.Age}"); }
C# 6 风格:
if (firstEmployee.GetType() == typeof(Employee)) { var employee = (Employee) firstEmployee; if (employee.Name == "Bassam Alugili" && employee.Age == 42) { Console.WriteLine($ "The employee: {employee.Name} , Age {employee.Age}"); } } // 或者我们可以这样做: var employee = firstEmployee as Employee; if (employee != null) { if (employee.Name == "Bassam Alugili" && employee.Age == 42) { Console.WriteLine($ "The employee: {employee.Name} , Age {employee.Age}"); } }
将模式匹配代码与 C# 6 进行比较,可以看出 C# 8 代码更加明晰。新的风格移除了冗余代码和类型转换以及丑陋的操作符,如“typeof”或“as”。
递归模式
递归模式只不过是上述模式的组合。类型将被分解为子部分,让子部分与子模式匹配。实际上,递归模式通过使用 Deconstruct() 方法来解构类型,并在必要时基于解构值进行进一步的模式匹配。如果你的类型没有 Deconstruct() 方法或者不是元组,那么就需要自己编写这个方法。
如果从上面的 Company 类中删除 Deconstruct 方法,则会出现以下错误:
error CS8129: No suitable Deconstruct instance or extension method was found for type ‘Company’, with 0 out parameters and a void return type。
接下来让我们来看看位置模式和属性模式。
示例
我创建了两个 Employee 对象和两个 Company 对象,并分别进行了映射。
var stratec = new Company { Name = "Stratec", Website = "wwww.stratec.com", HeadOfficeAddress = "Birkenfeld", }; var firstEmployee = new Employee { Name = "Bassam Alugili", Age = 42, Company = stratec }; var microsoft = new Company { Name = "Microsoft", Website = "www.microsoft.com", HeadOfficeAddress = "Redmond, Washington", }; var secondEmployee = new Employee { Name = "Satya Nadella", Age = 52, Company = microsoft }; DumpEmployee(firstEmployee); DumpEmployee(secondEmployee); public static void DumpEmployee(Employee employee) { switch (employee) { case Employee(_, _, _) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! "); } break; default: Console.WriteLine("Other company!"); break; } } Output The employee: Bassam Alugili The employee: Satya Nadella
在上面的示例中,case 将匹配包含数据的 Employee 对象,它是解构模式和丢弃模式的组合。现在我们将更进一步,只需要过滤 Stratec 的 employee。
使用模式匹配可以有多种方法。我们将使用一些不同的方式替换或重写以下的代码。
case Employee(_, _, _) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! "); } break;
第一种方法,在 switch 语句中使用递归模式匹配(解构模式),如下所示。
用以下代码替换上面的代码。
case Employee(_, _, ("Stratec", _, _)) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! "); } break;
输出:
The employee: Bassam Alugili! Other company!
第二种方法是使用警卫条件(Constraints)。
case Employee(_, _, (_, _, _)) employeeTmp when employeeTmp.Company.Name == "Stratec": { Console.WriteLine($ "The employee: {employeeTmp.Name}! "); } break;
同样,我们可以用不同的方式重写 case 表达式:
case Employee(_, _,_) employeeTmp when employeeTmp.Company.Name == "Stratec": case Employee employeeTmp when employeeTmp.Company.Name == "Stratec":
我们还可以将解构模式与变量模式结合起来,如下所示:
case Employee(_, _,var (_,companyNameTmp,_)) employeeTmp when companyNameTmp == "Stratec":
另一种通过递归属性模式来过滤数据的方法,如下所示:
case Employee {Company:Company{Name:"Stratec"}} employeeTmp: Output for the above examples: The employee: Bassam Alugili! Other company!
在将 switch 语句与模式匹配一起使用时,需要注意一个重要的事项:
新的 switch 表达式的结构如下所示:
switch (value) { case pattern guard => Code block to be executed ... case _ => default }
回到我们的示例,看看以下的递归模式匹配示例:
switch (employee) { case Employee {Name: "Bassam Alugili", Company: Company(_, _, _)} employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! 1"); } break; case Employee(_, _, ("Stratec", _, _)) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! 2"); } break; case Employee(_, _, Company(_, _, _)) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! 3"); } break; case Employee(_, _, _) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! 4"); } break; default: Console.WriteLine("Other company!"); break; }
上面的 switch 可以正常运行。如果我们将其中一个 case 向上或向下移动,比如将 case Employee(_,_,_) employeeTmp: 移动到开头,如下所示:
switch (employee) { case Employee(_,_,_) employeeTmp: { Console.WriteLine($ "The employee: {employeeTmp.Name}! 4"); } ... }
然后我们会得到以下错误:
- error CS8120: The switch case has already been handled by a previous case.
- error CS8120: The switch case has already been handled by a previous case.
- error CS8120: The switch case has already been handled by a previous case
图 4. 在 SharpLab 中移动 case 后出现的错误
编译器知道有些 case 是无法触及的(也就是死代码),并通过错误告诉你,你的代码写错了。
模式匹配与集合
示例
switch (intCollection) { case [1, 2, var x ] => { // 当 intCollection 中的头两个元素是 1 和 2 时,这个代码块会被执行,并且第 3 个元素会被复制给变量 x。 Console.WriteLine( $ "it's 1, 2, {x}", ); } case [1,..20] => { // 如果 intColleciton 以 1 为开头并以 20 结束,这个代码块会被执行。 ); case _ => { // 如果上述两个 case 不匹配,这执行这个代码块。 } } if (intCollection is [.., 99, 100]) { // 如果集合中的最后元素为 99 和 100,那么就执行这个代码块。 } if (intCollection is [1, 2, ..]) { // 如果集合中开始元素为 1 和 2,就执行这个代码块。 } if (intCollection is [1, .., 100]) { // 当集合中第一个元素是 1 并且最后一个元素是 100 时就执行这个代码块。 }
递归模式(C# 8)代码测试
- 复制以下代码示例
- 在 Web 浏览器中打开 https://sharplab.io
- 粘贴代码并选择“C# 8.0:RecusivePatterns(14 May 2018)”,然后选择“Run”,如图 5 所示。
或者,你可以使用我准备好的链接。
代码:
using System; namespace RecursivePatternsDemo { class Program { static void Main(string[] args) { var stratec = new Company { Name = "Stratec", Website = "wwww.stratec.com", HeadOfficeAddress = "Birkenfeld", }; var firstEmployee = new Employee { Name = "Bassam Alugili", Age = 42, Company = stratec }; var microsoft = new Company { Name = "Microsoft", Website = "www.microsoft.com", HeadOfficeAddress = "Redmond, Washington", }; var secondEmployee = new Employee { Name = "Satya Nadella", Age = 52, Company = microsoft }; DumpEmployee(firstEmployee); DumpEmployee(secondEmployee); } public static void DumpEmployee(Employee employee) { switch (employee) { case Employee {Name: "Bassam Alugili", Company: Company(_, _, _)} employeeTmp: { Console.WriteLine($"The employee: {employeeTmp.Name}! 1"); } break; case Employee(_, _, ("Stratec", _, _)) employeeTmp: { Console.WriteLine($"The employee: {employeeTmp.Name}! 2"); } break; case Employee(_, _, Company(_, _, _)) employeeTmp: { Console.WriteLine($"The employee: {employeeTmp.Name}! 3"); } break; default: Console.WriteLine("Other company!"); break; } } } } public class Company { public string Name { get; set; } public string Website { get; set; } public string HeadOfficeAddress { get; set; } public void Deconstruct(out string name, out string website, out string headOfficeAddress) { name = Name; website = Website; headOfficeAddress = HeadOfficeAddress; } } public class Employee { public string Name { get; set; } public int Age { get; set; } public Company Company { get; set; } public void Deconstruct(out string name, out int age, out Company company) { name = Name; age = Age; company = Company; } }
图 5. SharpLab 设置
总结
在以集合或列表的形式生成数字序列时,Ranges 是非常有用的。将 Ranges 与每个循环或模式匹配等组合在一起,让 C#语法变得更加简洁易读。
递归模式是模式匹配的核心。模式匹配将运行时数据与任意数据结构进行比较,并将其分解为组成部分,或以不同的方式从数据中提取子数据,编译器将为你检查代码的逻辑。
递归模式是一个非常棒的功能,可以灵活地基于一系列条件对数据进行测试,并根据满足的条件执行进一步的计算。
关于作者
Bassam Alugili 是 STRATEC AG 的高级软件专家和数据库专家。STRATEC 是全自动分析仪系统、实验室数据管理软件和智能耗材的全球领先合作伙伴。
评论 1 条评论