C# 9.0 正式版新特性

      11月10日,C# 9.0已经正式发布。一些新的特性也随之而来,这个版本主要焦点放在了数据的简洁性和不可变性表达上。编程

1. init关键字数组

1.1 仅初始化属性 — init关键字ide

对象初始化方式对于建立对象来讲是一种很是灵活和可读的方式,特别对一口气建立含有嵌套结构的树型对象来讲更有用。一个简单的初始化例子以下:函数

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

原来要进行对象初始化,咱们不得不写一些含有set访问器的属性,而且在构造函数的初次调用中,经过给属性赋值来实现。spa

public class Person { public string? FirstName { get; set; } public string? LastName { get; set; } }

这种方式最大的局限就是,对于初始化来讲,属性必须是可变的,也就是说,set访问器对于初始化来讲是必须的。而其余状况下又不须要set,所以这个setter就不合适了。为了解决这个问题,仅仅只用来初始化的init访问器出现了.。例如:命令行

public class Person { public string? FirstName { get; init; } public string? LastName { get; init; } }

init访问器是一个只在对象初始化时用来赋值的set访问器的变体,而且除过初始化进行赋值外,后续其余的赋值操做是不容许的。上面定义的Person对象,在下面代码中第一行初始化能够,第二行再次赋值就不被容许了。  设计

var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK
person.LastName = "Torgersen"; // ERROR!

 所以,一旦初始化完成以后,仅初始化属性就保护着对象免于改变code

1.2 init属性访问器和只读字段 对象

由于init访问器只能在初始化时被调用,因此在init属性访问器中能够改变封闭类的只读字段。blog

public class Person { private readonly string firstName = "<unknown>"; private readonly string lastName = "<unknown>"; public string FirstName { get => firstName; init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName))); } public string LastName { get => lastName; init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName))); } }

 

2 记录 / Records

       传统面向对象的编程的核心思想是一个对象有着惟一标识,封装着随时可变的状态。C#也是一直这样设计和工做的。可是一些时候,你就很是须要恰好对立的方式。原来那种默认的方式每每会成为阻力,使得事情变得费时费力。若是你发现你须要整个对象都是不可变的,且行为像一个值,那么你应当考虑将其声明为一个record类型。 

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

       一个record仍然是一个类,可是关键字record赋予这个类额外的几个像值的行为。一般说,records由他们的内容来界定,不是他们的标识。从这一点上讲,records更接近于结构,可是他们依然是引用类型。

2.1 with表达式

当使用不可变的数据时,一个常见的模式是从现存的值建立新值来呈现一个新状态。例如,若是Person打算改变他的姓氏(last name),咱们就须要经过拷贝原来数据,并赋予一个不一样的last name值来呈现一个新Person。这种技术被称为非破坏性改变。做为描绘随时间变化的person,record呈现了一个特定时间的person的状态。为了帮助进行这种类型的编程,records就提出了一个新的表达式——with表达式: 

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };

 

with表达式使用初始化语法来讲明新对象在哪里与原来对象不一样。with表达式其实是拷贝原来对象的整个状态值到新对象,而后根据对象初始化器来改变指定值。这意味着属性必须有init或者set访问器,才能用with表达式进行更改。

一个record隐式定义了一个带有保护访问级别的“拷贝构造函数”,用来将现有record对象的字段值拷贝到新对象对应字段中:

protected Person(Person original) { /* 拷贝全部字段 */ } // generated

with表达式就会引发拷贝构造函数被调用,而后应用对象初始化器来有限更改属性相应值。若是你不喜欢默认的产生的拷贝构造函数,你能够自定以,with表达式也会进行调用。

 

2.2 基于值的相等

      全部对象都从object类型继承了 Equals(object),这是静态方法 Object.Equals(object, object) 用来比较两个非空参数的基础。

结构重写了这个方法,经过递归调用每一个结构字段的Equals方法,从而有了“基于值的相等”,Recrods也是这样。这意味着只要他们的值保持一致,两个record对象能够不是同一个对象就会相等。例如咱们将修改的Last name又修改回去了:

 

var originalPerson = otherPerson with { LastName = "Nielsen" };

 

如今咱们会获得 ReferenceEquals(person, originalPerson) = false (他们不是同一对象),可是 Equals(person, originalPerson) = true (他们有一样的值).。与基于值的Equals一块儿的,还伴有基于值的GetHashCode()的重写。另外,records实现了IEquatable<T>并重载了==和 !=这两个操做符,以便于基于值的行为在全部的不一样的相等机制方面显得一致。

基于值的相等和可变性不老是契合的很好。一个问题是改变值可能引发GetHashCode的结果随时变化,若是这个对象被存放在哈希表中,就会出问题。咱们没有不容许使用可变的record,可是咱们不鼓励那样作,除非你已经想到了后果。

若是你不喜欢默认Equals重写的字段与字段比较行为,你能够进行重写。你只须要认真理解基于值的相等时如何在records中工做原理,特别是涉及到继承的时候,后面咱们会提到。

 

2.3 继承 / Inheritance

 记录(record)能够从其余记录(record)继承:

public record Student : Person
{
    public int ID;
}

with表达式和值相等性与记录的继承结合的很好,由于他们考虑到了整个运行时对象,不仅是静态的已知类型。好比,我建立一个Student对象,将其存在Person变量里。

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

with表达式仍然拷贝整个对象并保持着运行时的类型:

var otherStudent = student with { LastName = "Torgersen" };
WriteLine(otherStudent is Student); // true

一样地,值相等性确保两个对象有着一样的运行时类型,而后比较他们的全部状态:

Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };
WriteLine(student != similarStudent); //true, 因为ID值不一样

 

2.4  位置记录 / Positional records

 有时,有更多的位置定位方式对一个记录是颇有用的,在那里,记录的内容是经过构造函数的参数传入,而且经过位置解构函数提取出来。你彻底可能会在记录中定义你本身的构造和解构函数(注意不是析构函数)。以下所示:

 

public record Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; }
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

 

也能够用更精简的语法表达上面一样的内容。

public record Person(string FirstName, string LastName);

该方式声明了公开的、仅仅初始化的自动属性、构造函数和解构函数,和2.1种第一行代码带有大括号的声明方式不一样。如今你就能够写以下代码:

var person = new Person("Mads", "Torgersen"); // 位置构造函数 / positional construction
var (f, l) = person;                        // 位置解构函数 / deconstruction

固然,若是你不喜欢产生的自动属性,你能够你本身自定义的同名属性代替,产生的构造函数和解构函数将会只使用你自定义的那个。在这种状况下,该参数处于你用于初始化的做用域内。例如,你想让FirstName是个保护属性:

public record Person(string FirstName, string LastName)
{
    protected string FirstName { get; init; } = FirstName; 
}

一个位置记录能够像下面这样调用父类构造函数。

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

 

3 顶层程序(Top-Level Programs)

一般,咱们写一个简单的C#程序,都必然会有大量的代码: 

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

 

这个不只对于初学者来讲麻烦,并且使得代码凌乱,而且增长了缩进层级。在C#9.0中,你能够选择在顶层用以下代码代替写你的主程序:

using System;

Console.WriteLine("Hello World!");

固然,任何语句都是容许的。可是这段代码必须放在using后,和任何类型或者命名空间声明的前面。而且你只能在一个文件里面这样作,像现在只能写一个main方法同样。

若是你想返回状态,你能够那样作;你想用await,也能够那样作。而且,若是你想访问命令行参数,神奇的是,args像魔法同样也是可用的。

 

using static System.Console;
using System.Threading.Tasks;

WriteLine(args[0]);
await Task.Delay(1000);
return 0;

 本地函数做为语句的另外一种形式,也是容许在顶层程序代码中使用的。在顶层代码段外部的任何地方调用他们都会产生错误。

4 加强的模式匹配

C#9.0添加了几种新的模式。若是要了解下面代码段的上下文,请参阅模式匹配教程 

public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
       ...
       
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

 

(1)简单类型模式

当前,进行类型匹配的时候,一个类型模式须要声明一个标识符——即便这标识符是一个弃元_,像上面代码中的DeliveryTruck _ 。可是在C#9.0中,你能够只写类型,以下所示: 

DeliveryTruck => 10.00m,

 

(2)关系模式

C#9.0 提出了关系运算符<,<=等对应的模式。因此你如今能够将上面模式中的DeliveryTruck部分写成一个嵌套的switch表达式 

DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},

 这里 > 5000 和 < 3000就是关系模式。

 

(3)逻辑模式

       最后,你能够用逻辑操做符and,or 和not将模式进行组合,这里的操做符用单词来表示,是为了不与表达式操做符引发混淆。例如,上面嵌套的的switch能够按照升序排序,以下: 

DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},

        中间的分支使用了and 来组合两个关系模式来造成了一个表达区间的模式。

        not模式的常见的使用是将它用在null常量模式上,如not null。例如咱们要根据是否为空来把一个未知分支的处理进行拆分: 

not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))

 

          在包含了is表达式的if条件语句中,用于取代笨拙的双括号,使用not也会很方便:

if (!(e is Customer)) { ... }

          你能够这样写:

if (e is not Customer) { ... }

            实际上,在is not表达式里,容许你给Customer指定名称,以便后续使用。

if (e is not Customer c) { throw ... } // 若是这个分支抛出异常或者返回...
var n = c.FirstName; // ... 这里,c确定已经被赋值了,不会为空

 

5 类型推导new表达式

类型推导是从一个表达式所在的位置根据上下文得到它的类型时使用的一个术语。例如null和lambda表达式老是涉及到类型推导的。

在C#中,new表达式老是要求一个具体指定的类型(除了隐式类型数组表达式)。如今,若是表达式被指派给一个明确的类型时,你能够忽略new关键字后面的类型。

Point p = new (3, 5);

当有大量重复,这个特别有用。例以下面数组初始化:

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) }; 

 

返回值类型支持协变

有时候,在子类的一个重写方法中返回一个更具体的、且不一样于父类方法的返回类型更为有用,C# 9.0对这种状况提供了支持。以下列子中,子类Tiger的在重写父类Animal的GetFood方法时,返回值使用了Meat而不是Food,就更为形象具体。 

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}