C # 9.0的record

官方消息: c # 9.0已通过时了!早在五月份,我就在博客中介绍了 c # 9.0计划,下面是该文章的更新版本,以便与咱们最终发布的计划相匹配。web

对于每个新的 c # 版本,咱们都在努力提升常见编码场景的清晰度和简单性,c # 9.0也不例外。此次的一个特别重点是支持数据形状的简洁和不可变的表示。express

Init-only properties

对象初始化器很是棒。它们为类型的客户端建立对象提供了一种很是灵活和可读的格式,特别适用于嵌套对象建立,在嵌套对象建立过程当中,能够一次性建立整个对象树。下面是一个简单的例子:编程

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

对象初始值设定项还可使类型做者免于编写大量构造样板文件——他们所要作的就是编写一些属性!数组

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

目前的一个重大限制是,属性必须是可变的,对象初始化器才能工做: 它们的工做方式是首先调用对象的构造函数(本例中是缺省的、无参数的构造函数) ,而后分配给属性设置器。只有 init 属性能够解决这个问题!它们引入了一个 init 访问器,这是 set 访问器的一个变体,只能在对象初始化期间调用:ide

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

经过这个声明,上面的客户端代码仍然是合法的,可是任何后续对 FirstName 和 LastName 属性的赋值都是错误的:svg

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

所以,只初始化属性保护对象的状态在初始化完成后不会发生变异。函数

Init accessors and readonly fields Init (访问器和只读字段)

由于只能在初始化期间调用 init 访问器,因此容许它们改变封闭类的只读字段,就像在构造函数中同样。this

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

Records

经典面向对象程序设计的核心思想是,对象具备强大的身份,并封装了随时间演变的可变状态。C # 在这方面一直颇有效,可是有时候你想要的偏偏相反,这里 c # 的默认设置每每会妨碍工做,让事情变得很是艰难。编码

若是你发现本身但愿整个对象是不可变的,而且表现得像一个值,那么你应该考虑将它声明为一个记录:spa

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

记录仍然是一个类,可是记录关键字为它灌输了一些附加的类值行为。通常来讲,记录是由它们的内容而不是它们的身份来定义的。在这方面,记录更接近于结构,但记录仍然是引用类型。

虽然记录是可变的,可是它们主要是为了更好地支持不可变数据模型而构建的。

With-expressions

在处理不可变数据时,一个常见的模式是从现有数据建立新的值来表示新的状态。例如,若是咱们的人要改变他们的姓氏,咱们会将其表示为一个新对象,这个对象是旧对象的副本,只是姓氏不一样。这种技术一般被称为非破坏性突变。记录表明的不是随着时间的推移而表明的人,而是表明人在给定时间的状态。为了帮助这种编程风格,记录容许一种新的表达式: with-expression:

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

With-expressions 使用对象初始值设定项语法来讲明新对象与旧对象的不一样之处。能够指定多个属性。

With-expression 的工做方式是将旧对象的完整状态复制到新对象中,而后根据对象初始值设定项对其进行变异。这意味着属性必须有一个 init 或 set 访问器才能在 with-表达式中更改。

Value-based equality

全部对象都从对象类继承一个虚 Equals (对象)方法。这被用做 Object 的基础。当两个参数都非空时,Equals (object,object)静态方法。结构会重写这个函数,使其具备“基于值的相等性”,并经过递归地调用 Equals 对结构的每一个字段进行比较。唱片也是如此。这意味着,根据它们的“值性”,两个记录对象能够相等而不是同一个对象。例如,若是咱们再次修改被修改人的姓氏:

咱们如今有 ReferenceEquals (person,originalPerson) = false (它们不是同一个对象) ,但 Equals (person,originalPerson) = true (它们具备相同的值)。除了基于价值的 Equals 以外,还有一个基于价值的 GetHashCode ()覆盖。此外,记录实现了 IEquatable < t > 并使 = = 和!= 操做符,所以基于价值的行为在全部这些不一样的平等机制中一致地显示出来。

价值等同性和易变性并不总能很好地结合在一块儿。一个问题是,更改值可能会致使 GetHashCode 的结果随着时间的推移而更改,若是对象存储在哈希表中,这将是不幸的!咱们不由止可变记录,可是咱们不鼓励它们,除非您已经考虑到了后果!

Inheritance (继承)

记录能够从其余记录继承:

public record Student : Person
{
    public int ID;
}

使用-表达式和值相等能够很好地处理记录继承,由于它们考虑了整个运行时对象,而不只仅是静态地知道它的类型。假设我建立了一个 Student,可是把它存储在一个 Person 变量中:

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

一个 with-expression 仍然会复制整个对象并保持运行时类型:

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, since ID's are different

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);

这声明了公共 init-only auto-properties、构造函数和解构函数,以便您能够编写:

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

若是不喜欢生成的 auto-property,能够改成定义本身的同名属性,生成的构造函数和解构函数将只使用该属性。在这种状况下,您可使用该参数进行初始化。好比说,你但愿 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);

Top-level programs

用 c # 编写一个简单的程序须要大量的样板代码:

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

这不只对语言初学者来讲是压倒性的,并且会使代码变得杂乱无章,增长缩进的级别。在 c # 9.0中,你只须要在顶层编写你的主程序:

using System;

Console.WriteLine("Hello World!");

任何声明都是容许的。程序必须在使用以后以及文件中的任何类型或名称空间声明以前执行,并且只能在一个文件中执行此操做,就像如今只能有一个 Main 方法同样。若是您想返回状态代码,您能够这样作。若是你想等待,你能够这样作。若是您想访问命令行参数,可使用 args 做为“ magic”参数。

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

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

局部函数是一种语句形式,在顶级程序中也是容许的。从顶级语句部分之外的任何地方调用它们都是错误的。

Improved pattern matching

在 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))
    };

Simple type patterns

之前,当类型匹配时,类型模式须要声明一个标识符——即便该标识符是一个丢弃的 _,如上面的 DeliveryTruck _ 中所示。可是如今你能够只写类型:

DeliveryTruck => 10.00m,

Relational patterns 关系模式

C # 9.0引入了与关系运算符 < 、 < = 等对应的模式。因此你如今能够把上面模式的 DeliveryTruck 部分写成一个嵌套的开关表达式:

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

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

Logical patterns

最后,您能够将模式与逻辑运算符组合起来,而且(或者)做为单词拼写,以免与表达式中使用的运算符混淆。例如,上面的嵌套开关能够按以下升序排列:

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

这里的中间格使用和结合两个关系模式,并造成一个表示区间的模式。Not 模式的一个常见用法是将其应用于 null 常量模式,如 not null。例如,咱们能够根据未知状况是否为空来分割处理:

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

在 if-conditions 中包含 is-expressions,而不是笨拙的双括号,这样也不方便:

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

你能够直接说

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

事实上,在 is not 这样的表达式中,咱们容许您为 Customer 命名以供后续使用:

if (e is not Customer c) { throw ... } // if this branch throws or returns...
var n = c.FirstName; // ... c is definitely assigned here

Target-typed

“ Target typing”是一个术语,用于表达式从使用它的上下文中获取其类型。例如,null 和 lambda 表达式老是目标类型的。

C # 中的新表达式老是要求指定一个类型(隐式类型数组表达式除外)。在 c # 9.0中,若是表达式被赋值为一个明确的类型,则能够省略该类型。

Point p = new (3, 5);

当你有不少重复的时候,好比在数组或者对象初始值设定项中,这个特别好:

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

Covariant returns

有时表示派生类中的重写方法具备比基类中的声明更具体的返回类型是有用的。9.0容许:

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

还有更多…

查看完整的 c # 9.0特性的最佳位置是“ c # 9.0的新功能”文档页面。

相关文章
相关标签/搜索