[翻译]欢迎使用C#9.0
本文由公众号[开发者精选资讯](微信号:yuantoutiao)翻译首发,转载请注明来源
C# 9.0 is taking shape, and I’d like to share our thinking on some of the major features we’re adding to this next version of the language.
C#9.0初具规模,我想就我们要添加到该语言下一版本中的一些主要功能分享我们的想法。
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#的每个新版本中,我们都在通用编码方案中力求更加清晰和简单,C#9.0也不例外。这次的一个特别重点是支持数据类的简洁和不变数据的表示形式。
Let’s dive in!
让我们开始吧!
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:
对象初始化器非常棒。它们为类型实例提供了一种非常灵活且易于读取的格式来创建对象,并且特别适合嵌套对象的创建,在该对象中一次性创建了整个对象树。这是一个简单的例子:
new Person
{
FirstName = "Scott",
LastName = "Hunter"
}
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.
现在的一大局限性在于,属性必须是可变的,对象初始化程序才能起作用:它们通过首先调用对象的构造函数(在这种情况下为默认的,无参数的)来工作,然后赋值给属性setter方法。
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
属性的任何赋值都是错误的。
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
访问器只能在初始化期间被调用,因此允许它们改变封闭类readonly
的字段,就像在构造函数中一样。
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)));
}
}
Records
记录类
Init-only properties are great if you want to make individual properties immutable. If you want the whole object to be immutable and behave like a value, then you should consider declaring it as a record:
如果要使单个属性不变,则仅初始化属性非常有用。如果您希望整个对象是不可变的并且表现得像一个值,那么您应该考虑将其声明为记录:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
The data
keyword on the class declaration marks it as a record. This imbues it with several additional value-like behaviors, which we’ll dig into in the following. Generally speaking, records are meant to be seen more as “values” – data! – and less as objects. They aren’t meant to have mutable encapsulated state. Instead you represent change over time by creating new records representing the new state. They are defined not by their identity, but by their contents.
在类声明时使用data关键词标记一个记录类。这使它附加了其他一些类似于值的行为,我们将在下面对此进行深入研究。一般而言,记录类应更多地视为“值” –数据!–而不是作为对象。也就是说这个类成了不可改变的状态。您可以通过创建表示新状态的新记录来表示随着时间的变化的记录。它们不是由其身份定义的,而是由其内容定义的。
With-expressions
with 表达式
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 otherPerson = person with { LastName = "Hanselman" };
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-expressions使用对象初始化器语法来声明新对象与旧对象的不同之处。您可以指定多个属性。
A record implicitly defines a protected
“copy constructor” – a constructor that takes an existing record object and copies it field by field to the new one:
一条记录隐式定义了一个protected
“复制构造函数” –一种构造函数,它接受现有的记录对象并将其逐字段复制到新的记录对象中:
protected Person(Person original) { /* copy all the fields */ } // generated
The with
expression causes the copy constructor to get called, and then applies the object initializer on top to change the properties accordingly.
该with
表达式使副本构造函数被调用,然后在顶部应用对象初始化程序以相应地更改属性。
If you don’t like the default behavior of the generated copy constructor you can define your own instead, and that will be picked up by the with
expression.
如果您不喜欢所生成的副本构造函数的默认行为,则可以定义自己的副本构造函数,并将其由with
表达式提取。
值类型的相等性判断
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.
所有对象都从object类继承一个虚拟方法 Equals(object)。当两个参数都不为空时,它将用作静态方法 object
Object.Equals(object, object)
的基础相等性判断。
Structs override this to have “value-based equality”, comparing each field of the struct by calling Equals
on them recursively. Records do the same.
结构体重载此方法以具有“基于值的相等性”,通过Equals
递归调用结构来比较结构的每个字段。记录和结构体一样。
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:
这意味着,根据两个记录对象的值判断相等性,而不必是同一对象。例如,如果我们再次修改已修改人员的姓氏:
var originalPerson = otherPerson with { LastName = "Hunter" };
We would now have ReferenceEquals(person, originalPerson)
= false (they aren’t the same object) but Equals(person, originalPerson)
= true (they have the same value).
现在,我们将有 ReferenceEquals(person, originalPerson)= false(它们不是同一对象),但Equals(person, originalPerson)= true(它们具有相同的值)。
If you don’t like the default field-by-field comparison behavior of the generated Equals
override, you can write your own instead. You just need to be careful that you understand how value-based equality works in records, especially when inheritance is involved, which we’ll come back to below.
如果您不喜欢默认的逐域比较行为,则可以重写Equals方法。您只需要注意了解基于值的相等性在记录中的工作原理,尤其是在涉及继承时,我们将再下面文章中回到这个问题上。
Along with the value-based Equals
there’s also a value-based GetHashCode()
override to go along with it.
除了基于值判断的 Equals 方法外,
还有一个 GetHashCode() 方法,可以重写。
Data members
数据成员
Records are overwhelmingly intended to be immutable, with init-only public properties that can be non-destructively modified through with
-expressions. In order to optimize for that common case, records change the defaults of what a simple member declaration of the form string FirstName
means. Instead of an implicitly private field, as in other class and struct declarations, in records this is taken to be shorthand for a public, init-only auto-property! Thus, the declaration:
记录绝大多数都是不可变的,它们具有只能通过with
表达式进行非破坏性修改的仅初始化的公共属性。为了针对这种常见情况进行优化,记录更改了表单的简单成员声明的含义的默认值。代替其他类和结构声明中的隐式私有字段,在记录中将其视为公共的,仅用于初始化的自动属性的简写!因此,声明:string FirstName
public data class Person { string FirstName; string LastName; }
Means exactly the same as the one we had before:
与之前使用如下代码完全相同:
public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
We think this makes for beautiful and clear record declarations. If you really want a private field, you can just add the private
modifier explicitly:
我们认为这可以使记录声明优美而清晰。如果您确实想要私有字段,则可以private
显式添加修饰符:
private string firstName;
位置记录
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 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);
}
But there’s a much shorter syntax for expressing exactly the same thing (modulo casing of parameter names):
但是,用于表达完全相同的内容的语法要短得多(参数名称的模数框):
public data class Person(string FirstName, string LastName);
This declares the public init-only auto-properties and the constructor and the deconstructor, so that you can write:
这声明了仅用于初始化的公共自动属性以及构造函数和反构造函数,以便您可以编写:
var person = new Person("Scott", "Hunter"); // positional construction
var (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.
如果您不喜欢生成的自动属性,则可以定义自己的同名属性,使得生成的构造函数和反构造函数将仅使用该属性。
Records and mutation
记录和突变
The value-based semantics of a record don’t gel well with mutable state. Imagine putting a record object into a dictionary. Finding it again depends on Equals
and (sometimes) GethashCode
. But if the record changes its state, it will also change what it’s equal to! We might not be able to find it again! In a hash table implementation it might even corrupt the data structure, since placement is based on the hash code it has “on arrival”!
记录的基于值的语义与可变状态不能很好地融合在一起。想象一下将记录对象放入字典中。再次找到它取决于Equals
和(有时)GethashCode
。但是,如果记录更改其状态,则它也将更改其含义!我们可能无法再次找到它!在Hash表的实现中,它甚至可能破坏数据结构,因为放置是基于其“到达时”的Hash值!
There are probably some valid advanced uses of mutable state inside of records, notably for caching. But the manual work involved in overriding the default behaviors to ignore such state is likely to be considerable.
记录内部可能存在对可变状态的一些有效的高级用法,特别是用于缓存。但是,涉及覆盖默认行为以忽略这种状态的手动工作可能非常耗时。
With-expressions and inheritance
with 表达式和继承
Value-based equality and non-destructive mutation are notoriously challenging when combined with inheritance. Let’s add a derived record class Student
to our running example:
与继承相结合时,基于值的相等性和非破坏性突变是众所周知的挑战。让Student
我们在正在运行的示例中添加一个派生记录类:
public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }
And let’s start our with
-expression example by actually creating a Student
, but storing it in a Person
variable:
让我们开始创建with
表达式示例,方法是实际创建一个Student
,但将其存储在Person
变量中:
Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
otherPerson = person with { LastName = "Hanselman" };
At the point of that with
-expression on the last line the compiler has no idea that person
actually contains a Student
. Yet, the new person wouldn’t be a proper copy if it wasn’t actually a Student
object, complete with the same ID
as the first one copied over.
在最后一行的with表达式位置,编译器不知道person
实际上包含一个Student
。但是,如果新的 Person 类型实际上不是Student
对象,那么它就不是完全的副本,并且与ID
第一个被复制的对象完全相同。(这句有点拗口?)
C# makes this work. Records have a hidden virtual method that is entrusted with “cloning” the whole object. Every derived record type overrides this method to call the copy constructor of that type, and the copy constructor of a derived record chains to the copy constructor of the base record. A with
-expression simply calls the hidden “clone” method and applies the object initializer to the result.
C#做到了。记录具有一个隐藏的虚拟方法,该方法委托“克隆” 整个对象。每个派生记录类型都将重写此方法以调用该类型的副本构造函数,而派生记录的副本构造函数将链接到基本记录的副本构造函数。一个with
-expression只是调用隐藏的“克隆”的方法和适用对象初始化的结果。
Value-based equality and inheritance
值类型的相等性与继承
Similarly to the with
-expression support, value-based equality also has to be “virtual”, in the sense that Student
s need to compare all the Student
fields, even if the statically known type at the point of comparison is a base type like Person
. That is easily achieved by overriding the already virtual Equals
method.
与with
表达式支持的方式类似,值类型的相等性也必须是“虚拟的”,即Student
需要比较所有Student
字段,即使在比较时静态已知的类型是基本类型,例如Person
。通过覆盖已经存在的虚拟Equals
方法很容易实现。
However, there is an additional challenge with equality: What if you compare two different kinds of Person
? We can’t really just let one of them decide which equality to apply: Equality is supposed to be symmetric, so the result should be the same regardless of which of the two objects come first. In other words, they have to agree on the equality being applied!
但是,平等还有另外一个挑战:如果比较两种不同的类型Person
怎么办?我们不能真正让他们中的一个决定要应用哪个相等:相等应该是对称的,因此无论两个对象中的哪个首先出现,结果都应该相同。换句话说,他们必须就适用的相等达成一致!
An example to illustrate the problem:
一个例子来说明这个问题:
Person person1 = new Person { FirstName = "Scott", LastName = "Hunter" };
Person person2 = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
Are the two objects equal to one another? person1
might think so, since person2
has all the Person
things right, but person2
would beg to differ! We need to make sure that they both agree that they are different objects.
这两个对象彼此相等吗?person1
也许会这样想,因为person2
所有的Person
事情都对,person2
但愿与众不同!我们需要确保它们都同意它们是不同的对象。
Once again, C# takes care of this for you automatically. The way it’s done is that records have a virtual protected property called EqualityContract
. Every derived record overrides it, and in order to compare equal, the two objects musts have the same EqualityContract
.
C#再一次自动为您解决此问题。完成的方式是记录具有称为的虚拟受保护属性EqualityContract
。每个派生的记录都会覆盖它,并且为了比较相等,两个对象必须具有相同的EqualityContract
。
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 choose to 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 using
s 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.
允许任何语句。该程序必须在using
s之后并且在文件中任何类型或名称空间声明之前发生,并且您只能在一个文件中执行此操作,就像Main
今天只有一种方法一样。
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.
如果要返回状态代码,可以执行此操作。如果您想要await
做某事,您可以这样做。而且,如果要访问命令行参数,args
则可以作为“魔术”参数使用。
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
简单类型模式
Currently, 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:
当前,类型模式需要在类型匹配时声明一个标识符,即使该标识符是一个废弃_
(如上所述)。但是现在您可以这样编写类型:_
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
将上述模式的一部分编写为嵌套的switch表达式:
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.
这里和是关系模式是大于5000小于
3000
Logical patterns
逻辑匹配模式
Finally you can combine patterns with logical operators and
, or
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.
中间情况and
用来组合两个关系模式并形成表示间隔的模式。
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:
not
在包含is表达式的if条件中,代替笨重的双括号,以前这样写:
if (!(e is Customer)) { ... }
You can just say
现在可以这么写
if (e is not Customer) { ... }
Improved target typing
改进了目标类型(创建方式)
“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表达式始终是目标类型。
In C# 9.0 some expressions that weren’t previously target typed become able to be guided by their context.
在C#9.0中,某些以前不是目标类型的表达式可以通过其上下文进行引导。
Target-typed new
expressions
目标类型的new
表达式
new
expressions in C# have always required a type to be specified (except for implicitly typed array expressions). Now you can leave out the type if there’s a clear type that the expressions is being assigned to.
C#中的new表达式创建对象始终要求指定类型(隐式类型的数组表达式除外)。现在,如果有一个明确的类型要分配给表达式,则可以省去该类型。
Point p = new (3, 5);
Target typed ??
and ?:
目标类型??
和?:
Sometimes conditional ??
and ?:
expressions don’t have an obvious shared type between the branches. Such cases fail today, but C# 9.0 will allow them if there’s a target type that both branches convert to:
有时条件??
和?:
表达式在分支之间没有明显的共享类型。这种情况现在不允许,但是如果两个分支都转换为以下目标类型,则C#9.0将允许它们:
Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type
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() => ...;
}
And much more…
还有更多…
The best place to check out the full set of upcoming features for C# 9.0 and follow their completion is the Language Feature Status on the Roslyn (C#/VB Compiler) GitHub repo.
在Roslyn(C#/ VB编译器)GitHub存储库上,查看C#9.0即将推出的全部功能并完成这些功能的最佳场所。
原文:https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/