Build 2020上公布的C# 9.0 新特性

0
13

在微软的Build 2020开发者大会中,微软就正在成形的C#9.0的一些即将添加的主要特性进行了说明。

1.init属性访问器

对象初始化方式对于创建对象来说是一种非常灵活和可读的格式,特别是对树状嵌入型对象的创建。简单的例如

new Person
{
    FirstName = "Scott",
    LastName = "Hunter"
}

原有的要进行对象初始化,我们必须要做就是写一些属性,并且在构造函数的初次调用中,通过给属性的setter赋值来实现。

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

就这样的方式而言,set访问器对于初始化来说是必须的,但是如果你想要的是只读属性,这个set就是不合适的。除过初始化,其他情况不需要,而且很多情况下容易引起属性值改变。为了解决这个矛盾,只用来初始化的init访问器出现了。init访问器是一个只在对象初始化时用来赋值的set访问器的变体,并且后续的赋值操作是不允许的。例如:

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

2. init属性访问器和只读字段

因为init访问器只能在初始化时被调用,原来只能在构造函数里进行初始化的只读字段,现在可以在属性中进行初始化,不用再在构造函数进行初始化。省略了构造函数。

public class Person
{
    private readonly string firstName;
    private readonly string lastName;
    
    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)));
    }
}

3. Records

如果你需要一个整个对象都是不可变的,且行为像一个值的类型,你可以考虑将其声明为record。

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

关键字data用来将类标记为record。这样类就具有了像值一样的行为。records意味看起来更像值,也就是数据,而且很少像对象。不是说他们有可变的封装的状态,而是通过创建代表新状态的records来呈现随时间变化的状态。records不是被他们的标识符界定,而是被他们的内容所界定的。

4. With表达式

当用不可变的数据类型时,一个公用的模式是从现存的值类创建新值来呈现一个新状态。例如,如果Person改变了他的last name,我们就需要通过拷贝原来数据,并设置一个不同的last name来呈现一个新Person。这种技术被称为非破坏性改变。作为呈现随时间变化的person,record呈现了一个特定时间的person的状态。为了帮助这种类型的编程处理,records就提出了一个新的表达式,这就是with表达式:

var otherPerson = person with { LastName = "Hanselman" };

with表达式使用初始化语法来声明与原来对象不同的新状态对象。

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

protected Person(Person original) { /* copy all the fields */ } // generated

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

5. 基于值的相等

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

结构重写这个方法,通过递归调用每个结构字段的Equals方法,从而有了“基于值的相等”,Recrods也是这样。这意味着那样与他们无值保持一致,两个record对象可以不用是同一个对象,而且相等。例如我们修改回了last name:

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

现在我们会有ReferenceEquals(person,originalPerson)= false (他们不是同一对象),但是Equals(person,originalPerson)= true (他们有同样的值).。

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

与基于值的Equals一起的,还伴有基于值的GetHashCode()的重写。

6.data成员

不可变的Records的成员是带有init的公共属性,可以通过with表达式进行无破坏性修改的。为了优化这种共有情况,records改变了形如stringFirstName的默认意思,即在结构和类中声明的隐式私有字段,在records中成了公有,仅初始化自动属性。

public data class Person { string FirstName; string LastName; }

上面这段声明,就是跟下面这段代码意思相同:

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

这个使得record声明看起来优美而清晰直观。如果你真的需要一个私有字段,你可以显式添加private修饰符。

private string firstName;

7. Positional records

在record是可以指定构造函数和解构函数(注意不是析构函数)的。

public data class Person 
{ 
    string FirstName; 
    string LastName; 
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

也可以用更精简的语法表达上面同样的内容。

public data class Person(string FirstName, string LastName);

该方式声明了公开的带有初始化自动属性、构造函数和解构函数,和第6条第一段代码带有大括号的声明方式是不同的。现在你就可以写如下代码:

var person = new Person("Scott", "Hunter"); // positional construction
var (f, l) = person;                        // positional deconstruction

当然,如果你不喜欢产生的自动属性,你可以自定义你自己的同名属性代替,产生的构造函数和解构函数将会只使用那个。

8.Records和变化

record基于值这种语义没有很好应对可变状态这种情况。想象给字典插入一个record对象。要查找到它,就得根据Equals和(一些时候)GetHashCode。 但是如果record改变了状态,它们也会跟着改变。这样,我们就可能再找不到这个record。在哈希表实现中,这样可能会损害数据结构,由于放置位置是基于它到达的哈希码。

现实中,存在有一些record内部的可变状态的有效的高级应用,如缓存。但是在重写默认行为来忽略这种状态所涉及的人工工作可能是相当多的。

9. with表达式和继承

众所周知,基于值相等和非破坏性变化值的方式在和继承纠结在一起时,非常具有挑战性。下来,我们添加一个继承的record类Student来说明我们的例子:

public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }

下来,我们通过创建一个Student,但是把他存放到Person变量中,来说明with表达式的使用:

Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
otherPerson = person with { LastName = "Hanselman" };

在最后一行,编译器不知道person实际存放的是一个Student,这样,新person对象就不会得到正确的拷贝,即就不会像4中的第一段代码拷贝那样,得到同样ID值。

C#为了使这个得以正确工作。Records有一个隐藏的virtual方法,用于执行整体对象的克隆。每个派生的record类型重写了这个方法,以调用那个类型的拷贝构造函数,派少的record构造函数也受约束于父record的拷贝构造函数。with表达式简单调用这个隐藏的“clone”方法,应用对象初始化器给结果。

10. 基于值相等和继承

类似于with表达式的支持,基于值的相等性也必须是“virtual”,在这种意义上来说,Students需要比较所有student字段,即使在比较时静态已知类型是像Person这样的基类,也容易通过重写已经有的virtual Equals方法来实现。

然而,相等有着一个格外的挑战,就是如果比较两个不同类型的Person会怎么样?我们不能让他们中一个决定应用哪一个相等性:相等应该是语义的,所以不管两个对象中哪个先来,解构应是相同的。换句话说,他们必须在被应用的相等性达成一致。

这个问题的例子如下:

Person person1 = new Person { FirstName = "Scott", LastName = "Hunter" };
Person person2 = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };

这两个对象是不是彼此相等?person1可能这样认为,由于person2有着person所有的成员,并且值相等,但是person2不敢苟同。我们需要确认他们两个对于他们是不同对象达成一致意见。

再一次,C#自然为你考虑到了这个,实现的方式是records有一个虚拟保护的属性,叫做EqualityContract。每个派生的record重写它,以便比较相等,两个对象必须有同样的EqualityContract。

11.顶级程序

通常,我们洗一个简单的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也是可用的。

本地方法是语句的另一个形式,也是允许在顶级程序代码用的。在顶级代码段外部的任何地方调用他们都会产生错误。

12. 增强的模式匹配

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 推出了关系运算符相应的模式,例如<,<=等等。所以你现在可以用switch表达式将下上面模式中的DeliveryTruck部分写成下面样子:

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,
},

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

not模式的公同使用是它将会被用在null常量模式上,就像not null。例如我们要根据是否为空来分割一个未知的case处理代码段:

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

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

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

你可以这样写:

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

13. 增强的类型推导

类型推导是当一个表达式从它所被使用的地方的上下文中获得它的类型时,我们经常使用的一个专业术语。例如null和lambda表达式总是涉及到类型推导的。

在C#9.0中,先前没有实现类型推导的一些表达式现在也可以用他们的上下文来进行类型推导了。

(1)类型推导的new表达式

在C#中,new表达式总是要求一个具体指定的类型(除了隐式类型数组表达式)。现在,如果表达式被指派给一个明确的类型时,你可以忽略new中类型。

Point p = new (3, 5);

(2)类型推导的??和?:

一些时候,条件表达式??和?:在分支中没有明显的共享类型。现有这种情况会失败,但是在C#9.0中,如果各分支可以转换 为目标类型,这种情况时允许的。

Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type

14.支持协变的返回值

一些时候,在子类的一个重写方法中返回一个更具体的、且不同于父类方法定义的返回类型更为有用,C# 9.0对这种情况提供了支持。

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

<

发布回复

请输入评论!
请输入你的名字