58

C# 8的Ranges和递归模式

 6 years ago
source link: http://www.infoq.com/cn/articles/cs8-ranges-and-recursive-patterns?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关键要点

  • 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的新提议并未改变对特性的关注,但在最终版本中可能会有所改变。

 1661-1533399378164.jpg 

图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块。

尝试在这里 运行 上面的代码。

 ryMzeuY.png!web  5742-1533399378719.png

图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 

10 

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

Index 

10 

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

var slice= array[2..5];

Value 

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

Index 

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

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

注意:目前,负面界限无法测试,如下所示:

3MVnQff.png!web  4593-1533399379086.png

图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

854-1533399547366.jpg

图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;
 }
}

685-1533399547004.jpg

图5. SharpLab设置

总结

在以集合或列表的形式生成数字序列时,Ranges是非常有用的。将Ranges与每个循环或模式匹配等组合在一起,让C#语法变得更加简洁易读。

递归模式是模式匹配的核心。模式匹配将运行时数据与任意数据结构进行比较,并将其分解为组成部分,或以不同的方式从数据中提取子数据,编译器将为你检查代码的逻辑。

递归模式是一个非常棒的功能,可以灵活地基于一系列条件对数据进行测试,并根据满足的条件执行进一步的计算。

关于作者

2bassam-alugili-cs8-ranges-and-recursive-patterns-1532385273851.jpg Bassam Alugili  是STRATEC AG的高级软件专家和数据库专家。STRATEC是全自动分析仪系统、实验室数据管理软件和智能耗材的全球领先合作伙伴。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK