C# 9.0 on the record/新语法

C# 9.0 on the record/C#9.0记录在案

It’s official: C# 9.0 is out! Back in May I blogged about the C# 9.0 plans, and the following is an updated version of that post to match what we actually ended up shipping.

With every new version of C# we strive for greater clarity and simplicity in common coding scenarios, and C# 9.0 is no exception. One particular focus this time is supporting terse and immutable representation of data shapes.

官方消息:C#9.0已经发布!早在5月份,我就在博客上发表了关于C#9.0计划的文章,下面是这篇文章的更新版本,与我们最终发布的内容相匹配。

随着C的每一个新版本的出现,我们在常见的编码场景中都力求更加清晰和简单,C#9.0也不例外。这次的一个特别的焦点是支持数据形状的简洁和不可变的表示。

Init-only properties/仅初始化属性

Object initializers are pretty awesome. They give the client of a type a very flexible and readable format for creating an object, and they are especially great for nested object creation where a whole tree of objects is created in one go. Here’s a simple one:

对象初始化器非常棒。它们为类型的客户机提供了一种非常灵活和可读的创建对象的格式,尤其适用于一次性创建整个对象树的嵌套对象创建。这里有一个简单的例子:

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

Object initializers also free the type author from writing a lot of construction boilerplate – all they have to do is write some properties!

对象初始值设定项也让类型编写者从编写大量构造样板文件中解放出来——他们所要做的就是编写一些属性!

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

The one big limitation today is that the properties have to be mutable for object initializers to work: They function by first calling the object’s constructor (the default, parameterless one in this case) and then assigning to the property setters. Init-only properties fix that! They introduce an init accessor that is a variant of the set accessor which can only be called during object initialization:

今天一个很大的限制是属性必须是可变的,以便对象初始值设定项工作:它们首先调用对象的构造函数(在本例中是默认的无参数构造函数),然后分配给属性设置器。只初始化属性修复这个问题!它们引入了一个init访问器,它是set访问器的一个变体,只能在对象初始化期间调用:

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

With this declaration, the client code above is still legal, but any subsequent assignment to the FirstName and LastName properties is an error:

使用此声明,上面的客户端代码仍然是合法的,但是对FirstName和LastName属性的任何后续赋值都是错误的:

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

Thus, init-only properties protect the state of the object from mutation once initialization is finished.

因此,只有init属性在初始化完成后保护对象的状态不受变化的影响。

Init accessors and readonly fields/初始化访问器和只读字段

Because init accessors can only be called during initialization, they are allowed to mutate readonly fields of the enclosing class, just like you can in a constructor.

因为init访问器只能在初始化期间调用,所以允许它们改变封闭类的只读字段,就像在构造函数中一样。

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/记录

At the core of classic object-oriented programming is the idea that an object has strong identity and encapsulates mutable state that evolves over time. C# has always worked great for that, But sometimes you want pretty much the exact opposite, and here C#’s defaults have tended to get in the way, making things very laborious.

经典的面向对象编程的核心思想是一个对象具有很强的一致性,并且封装了随时间变化的可变状态。C一直在这方面工作得很好,但有时你想要的恰恰相反,而在这里,C的默认设置往往会妨碍工作,使事情变得非常麻烦。

If you find yourself wanting the whole object to be immutable and behave like a value, then you should consider declaring it as a record:

如果您发现自己希望整个对象是不可变的,并且行为类似于一个值,那么您应该考虑将它声明为一个记录:

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

A record is still a class, but the record keyword imbues it with several additional value-like behaviors. Generally speaking, records are defined by their contents, not their identity. In this regard, records are much closer to structs, but records are still reference types.

记录仍然是一个类,但是record关键字为它注入了一些附加的类似值的行为。一般来说,记录是由其内容而不是其身份来定义的。在这方面,记录更接近于结构,但记录仍然是引用类型。

While records can be mutable, they are primarily built for better supporting immutable data models.

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

With-expressions/带表达式

When working with immutable data, a common pattern is to create new values from existing ones to represent a new state. For instance, if our person were to change their last name we would represent it as a new object that’s a copy of the old one, except with a different last name. This technique is often referred to as non-destructive mutation. Instead of representing the person over time, the record represents the person’s state at a given time. To help with this style of programming, records allow for a new kind of expression; the with-expression:

在处理不可变数据时,一个常见的模式是从现有值中创建新值来表示新状态。例如,如果我们的人要更改他们的姓氏,我们会将其表示为一个新对象,它是旧对象的副本,但姓氏不同。这种技术通常被称为非破坏性突变。记录不是代表一段时间内的人,而是代表该人在给定时间的状态。为了帮助实现这种编程风格,记录允许使用一种新的表达式;with表达式:

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

With-expressions use object initializer syntax to state what’s different in the new object from the old object. You can specify multiple properties.

With表达式使用对象初始值设定项语法来说明新对象与旧对象的不同之处。可以指定多个属性。

The with-expression works by actually copying the full state of the old object into a new one, then mutating it according to the object initializer. This means that properties must have an init or set accessor to be changed in a with-expression.

with表达式的工作原理是将旧对象的完整状态复制到新对象中,然后根据对象初始值设定项对其进行变异。这意味着属性必须具有要在with表达式中更改的init或set访问器。

Value-based equality/基于价值的平等

All objects inherit a virtual Equals(object) method from the object class. This is used as the basis for the Object.Equals(object, object) static method when both parameters are non-null. Structs override this to have "value-based equality", comparing each field of the struct by calling Equals on them recursively. Records do the same. This means that in accordance with their "value-ness" two record objects can be equal to one another without being the same object. For instance if we modify the last name of the modified person back again:

所有对象都从对象类继承一个虚拟Equals(object)方法。这被用作对象。等于(object,object)两个参数都非null时的静态方法。结构重写此项以具有“基于值的相等”,通过递归调用Equals来比较结构的每个字段。记录也是如此。这意味着,根据它们的“值”,两个记录对象可以彼此相等,而不是同一个对象。例如,如果我们再次修改被修改人的姓氏:

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

We would now have ReferenceEquals(person, originalPerson) = false (they aren’t the same object) but Equals(person, originalPerson) = true (they have the same value). Along with the value-based Equals there’s also a value-based GetHashCode() override to go along with it. Additionally, records implement IEquatable<T> and overload the == and != operators, so that the value-based behavior shows up consistently across all those different equality mechanisms.

我们现在的ReferenceEquals(person,originalPerson)=false(它们不是同一个对象),而是Equals(person,originalPerson)=true(它们具有相同的值)。除了基于值的Equals之外,还有一个基于值的GetHashCode()重写。另外,记录实现了IEquatable<T>并重载==和!=运算符,以便基于值的行为在所有这些不同的等式机制中一致地显示出来。

Value equality and mutability don’t always mesh well. One problem is that changing values could cause the result of GetHashCode to change over time, which is unfortunate if the object is stored in a hash table! We don’t disallow mutable records, but we discourage them unless you have thought through the consequences!

值的相等性和可变性并不总是很好地结合在一起。一个问题是,更改值可能会导致GetHashCode的结果随时间而改变,如果对象存储在哈希表中,这是不幸的!我们不禁止可变记录,但我们不鼓励他们,除非你考虑过后果!

Inheritance/继承

Records can inherit from other records:

记录可以从其他记录继承:

public record Student : Person{
    public int ID;}

With-expressions and value equality work well with record inheritance, in that they take the whole runtime object into account, not just the type that it’s statically known by. Say that I create a Student but store it in a Person variable:

With表达式和值相等与记录继承配合得很好,因为它们考虑了整个运行时对象,而不仅仅是静态已知的类型。假设我创建了一个学生,但将其存储在Person变量中:

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

A with-expression will still copy the whole object and keep the runtime type:

with表达式仍将复制整个对象并保留运行时类型:

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

In the same manner, value equality makes sure the two objects have the same runtime type, and then compares all their state:

以同样的方式,值相等确保两个对象具有相同的运行时类型,然后比较它们的所有状态:

Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };WriteLine(student != similarStudent); //true, since ID's are different

Positional records/位置记录

Sometimes it’s useful to have a more positional approach to a record, where its contents are given via constructor arguments, and can be extracted with positional deconstruction. It’s perfectly possible to specify your own constructor and deconstructor in a record:

有时,对一个记录使用更具位置性的方法是很有用的,其中它的内容是通过构造函数参数给出的,并且可以通过位置解构来提取。完全可以在记录中指定自己的构造函数和解构器:

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

But there’s a much shorter syntax for expressing exactly the same thing (modulo casing of parameter names):

表示同一个参数的更短的语法:

public record Person(string FirstName, string LastName);

This declares the public init-only auto-properties and the constructor and the deconstructor, so that you can write:

这将声明public init only auto属性、构造函数和解构器,以便您可以编写:

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

If you don’t like the generated auto-property you can define your own property of the same name instead, and the generated constructor and deconstructor will just use that one. In this case, the parameter is in scope for you to use for initialization. Say, for instance, that you’d rather have the FirstName be a protected property:

如果您不喜欢生成的auto属性,您可以定义自己的同名属性,生成的构造函数和解构器将只使用该属性。在本例中,该参数在您用于初始化的范围内。例如,假设您希望名字是受保护的财产:

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

A positional record can call a base constructor like this:

位置记录可以像这样调用基构造函数:

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

Top-level programs/顶级课程

Writing a simple program in C# requires a remarkable amount of boilerplate code:

用C编写一个简单的程序需要大量的样板代码:

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

This is not only overwhelming for language beginners, but clutters up the code and adds levels of indentation. In C# 9.0 you can just write your main program at the top level instead:

这不仅对语言初学者来说是难以承受的,而且会使代码变得混乱,并增加缩进级别。在C#9.0中,您只需在顶层编写主程序:

using System;Console.WriteLine("Hello World!");

Any statement is allowed. The program has to occur after the usings and before any type or namespace declarations in the file, and you can only do this in one file, just as you can have only one Main method today. If you want to return a status code you can do that. If you want to await things you can do that. And if you want to access command line arguments, args is available as a "magic" parameter.

允许任何语句。程序必须在usings之后和文件中的任何类型或名称空间声明之前发生,并且只能在一个文件中执行此操作,就像今天只能有一个Main方法一样。如果你想返回一个状态码,你可以这样做。如果你想等待,你可以这样做。如果您想访问命令行参数,args可以作为“魔术”参数使用。

using static System.Console;using System.Threading.Tasks;WriteLine(args[0]);await Task.Delay(1000);return 0;

Local functions are a form of statement and are also allowed in the top level program. It is an error to call them from anywhere outside of the top level statement section.

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

Improved pattern matching/改进的模式匹配

Several new kinds of patterns have been added in C# 9.0. Let’s look at them in the context of this code snippet from the pattern matching tutorial:

在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/简单类型模式

Previously, a type pattern needs to declare an identifier when the type matches – even if that identifier is a discard _, as in DeliveryTruck _ above. But now you can just write the type:

以前,类型模式需要在类型匹配时声明一个标识符,即使该标识符是一个discard,就像上面的DeliveryTruck\一样。但现在你只需写下类型:

DeliveryTruck => 10.00m,

Relational patterns/关系模式

C# 9.0 introduces patterns corresponding to the relational operators <<= and so on. So you can now write the DeliveryTruck part of the above pattern as a nested switch expression:

C#9.0引入了与关系运算符<、<=等相对应的模式。因此,现在可以将上述模式的DeliveryTruck部分编写为嵌套的开关表达式:

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

Here > 5000 and < 3000 are relational patterns.

Logical patterns/逻辑模式

Finally you can combine patterns with logical operators andor and not, spelled out as words to avoid confusion with the operators used in expressions. For instance, the cases of the nested switch above could be put into ascending order like this:

最后,您可以将模式与逻辑运算符and、or和not组合在一起,以避免与表达式中使用的运算符混淆。例如,上面嵌套开关的情况可以按如下升序排列:

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

The middle case there uses and to combine two relational patterns and form a pattern representing an interval. A common use of the not pattern will be applying it to the null constant pattern, as in not null. For instance we can split the handling of unknown cases depending on whether they are null:

中间的大小写使用和组合两个关系模式,形成一个表示间隔的模式。not模式的一个常见用法是将其应用于null常量模式,如not null。例如,我们可以根据未知案例是否为空来拆分它们的处理:

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

Also not is going to be convenient in if-conditions containing is-expressions where, instead of unwieldy double parentheses:

如果条件中包含is表达式,而不是笨拙的双括号,则not也很方便:

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

You can just say

你可以说

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

And in fact, in an is not expression like that we allow you to name the Customer for subsequent use:

事实上,在这样一个is not表达式中,我们允许您命名客户以供后续使用:

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

Target-typed new expressions/目标类型的新表达式

"Target typing" is a term we use for when an expression gets its type from the context of where it’s being used. For instance null and lambda expressions are always target typed.

“目标类型”是一个术语,当一个表达式从使用它的地方的上下文中获得它的类型时,我们使用这个术语。例如,null和lambda表达式始终是目标类型的。

new expressions in C# have always required a type to be specified (except for implicitly typed array expressions). In C# 9.0 you can leave out the type if there’s a clear type that the expression is being assigned to.

C中的新表达式始终要求指定类型(隐式类型的数组表达式除外)。在C#9.0中,如果有一个明确的类型来指定表达式,则可以省略该类型。

Point p = new (3, 5);

This is particularly nice when you have a lot of repetition, such as in an array or object initializer:

当您有很多重复时,例如在数组或对象初始值设定项中,这尤其好:

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

Covariant returns/协变收益

It’s sometimes useful to express that a method override in a derived class has a more specific return type than the declaration in the base type. C# 9.0 allows that:

有时可以表示派生类中的方法重写具有比基类型中的声明更具体的返回类型。C#9.0允许:

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

作者:Mads

翻译:刘海鑫

猜你喜欢

转载自blog.csdn.net/u014249305/article/details/109912563
今日推荐