本文由公众号[开发者精选资讯](微信号:yuantoutiao)翻译首发,转载请注明来源ios
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.git
C#9.0初具规模,我想就咱们要添加到该语言下一版本中的一些主要功能分享咱们的想法。github
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.express
在C#的每一个新版本中,咱们都在通用编码方案中力求更加清晰和简单,C#9.0也不例外。此次的一个特别重点是支持数据类的简洁和不变数据的表示形式。编程
Let’s dive in!c#
让咱们开始吧!数组
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
属性的任何赋值都是错误的。
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))); } }
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关键词标记一个记录类。这使它附加了其余一些相似于值的行为,咱们将在下面对此进行深刻研究。通常而言,记录类应更多地视为“值” –数据!–而不是做为对象。也就是说这个类成了不可改变的状态。您能够经过建立表示新状态的新记录来表示随着时间的变化的记录。它们不是由其身份定义的,而是由其内容定义的。
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() 方法,能够重写。
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.
若是您不喜欢生成的自动属性,则能够定义本身的同名属性,使得生成的构造函数和反构造函数将仅使用该属性。
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.
记录内部可能存在对可变状态的一些有效的高级用法,特别是用于缓存。可是,涉及覆盖默认行为以忽略这种状态的手动工做可能很是耗时。
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只是调用隐藏的“克隆”的方法和适用对象初始化的结果。
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
。
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.
局部函数是语句的一种形式,而且在顶层程序中也容许使用。从顶级语句部分以外的任何地方调用它们是错误的。
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)) };
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,
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
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) { ... }
“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中,某些之前不是目标类型的表达式能够经过其上下文进行引导。
new
expressionsnew
表达式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);
??
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
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() => ...; }
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/