BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

C# 8的Ranges和递归模式

| 作者 Bassam Alugili 关注 1 他的粉丝 ,译者 无明 关注 1 他的粉丝 发布于 2018年8月8日. 估计阅读时间: 44 分钟 | BCCon2018全球区块链生态技术大会,将区块链技术的创新和早期落地案例带回您的企业

关键要点

  • C# 8新增了Ranges和递归模式。
  • 可以使用Ranges来定义数据序列,可用于替代Enumberable.Range()。
  • 递归模式为C#带来了类似F#的结构。
  • 递归模式是一个非常棒的功能,为我们提供了一种灵活的方式,基于一系列条件来测试数据,并根据满足的条件执行进一步的计算。
  • Ranges可用于生成集合或列表形式的数字序列。

2015年1月21日是C#历史上最重要的日子之一。在这一天,C#专家Anders Hejlsberg和Mads Torgersen等人聚在一起畅谈C#的未来,并思考了这门语言应该往哪个方向发展。

2015年1月21日的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 thirdItem = array [2]; 

// 后台的代码: array [2]

var thirdItem = array [2]; 

var lastItem = array [^1];

// 后台的代码: [^1] = new Index(1, true);

var lastItem = array [array.Count -1];

var lastItem = array.Last; // LINQ

var subCollection = array[2..^5]; // 输出: 2, 3, 4, 5

// 后台的代码: Range.Create(2, new Index(5, true)); 我们使用了两种操作符Range和Index。Range对应操作符..Index对应操作符^。意思是从头开始跳到索引2的位置,^5表示忽略从头开始的5个元素。

var subCollection = array.ToList().GetRange(2, 4);

使用LINQ就是:

var subCollection = array.Skip(2).Take(4);

示例

考虑下面的数组:

var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 

 

 Value 

 0 

 1 

 2 

 3 

 4 

 5 

 6 

 7 

 8 

 9 

 10 

我们可以使用以下索引访问数组的值:

 Index 

 0 

 1 

 2 

 3 

 4 

 5 

 6 

 7 

 8 

 9 

 10 

现在,我们从这个数组中剪切出一个切片视图,如下所示:

var slice= array[2..5];

 Value 

 2 

 3 

 4 

我们可以使用以下索引访问切片的值:

 Index 

 0 

 1 

 2 

注意:起始索引是被包含在切片中的,而结束索引是不包含在切片中的。

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");
  }
  ...
}

然后我们会得到以下错误:

  1. error CS8120: The switch case has already been handled by a previous case.
  2. error CS8120: The switch case has already been handled by a previous case.
  3. 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)代码测试

  1. 复制以下代码示例
  2. 在Web浏览器中打开https://sharplab.io
  3. 粘贴代码并选择“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是全自动分析仪系统、实验室数据管理软件和智能耗材的全球领先合作伙伴。

 

查看英文原文C# 8 Ranges and Recursive Patterns

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT