C#9.0正在成型,我想与您分享我们对该语言下一版本中添加的一些主要特性的看法。
对于每个新版本的C#,我们都努力在常见的编码场景中实现更高的清晰度和简单性,C#9.0也不例外。这一次的一个特别焦点是支持数据形状的简洁和不可变的表示形式。
对象初始化器非常棒。它们为某种类型的客户端提供了一种非常灵活且可读的格式来创建对象,尤其适用于一次性创建整个对象树的嵌套对象创建。这里有一个简单的例子:
对象初始化器还将类型作者从编写大量构造样板中解放出来-他们所要做的就是编写一些属性!
公共类Person{public String FirstName{get;Set;}公共字符串LastName{get;Set;}}。
目前的一大限制是属性必须是可变的,对象初始化器才能工作:它们的功能是首先调用对象的构造函数(在本例中是缺省的、无参数的构造函数),然后将其分配给属性设置器。
仅初始化属性解决了这个问题!它们引入了一个初始化访问器,它是set访问器的变体,只能在对象初始化期间调用:
公共类Person{public String FirstName{get;init;}公共字符串LastName{get;init;}}
使用此声明,上面的客户端代码仍然是合法的,但是任何后续对FirstName和LastName属性的赋值都是错误的。
因为初始化访问器只能在初始化期间调用,所以允许它们改变封闭类的只读字段,就像您在构造函数中可以做的那样。
公共类Person{private readonly string firstName;private readonly string lastName;public string firstName{get=>;firstName;init=>;firstName=(value??抛出新的ArgumentNullException(nameof(FirstName));}公共字符串LastName{get=>;lastName;init=>;lastName=(value??抛出新的ArgumentNullException(nameof(LastName));}}。
如果您希望使各个属性不可变,则仅初始化属性非常有用。如果您希望整个对象是不可变的,并且行为类似于一个值,那么您应该考虑将其声明为记录:
公共数据类Person{public String FirstName{get;init;}公共String LastName{get;init;}}。
类声明上的DATA关键字将其标记为记录。这给它注入了几个额外的类似值的行为,我们将在下面深入研究这些行为。一般来说,记录更多地被视为“值”--数据!--而不是对象。它们不应该具有可变的封装状态。相反,您可以通过创建表示新状态的新记录来表示随时间的变化。它们不是由它们的身份来定义的,而是由它们的内容来定义的。
在处理不可变数据时,一种常见的模式是从现有值创建新值来表示新状态。例如,如果我们的人要更改他们的姓氏,我们会将其表示为一个新对象,该对象是旧对象的副本,只是使用了不同的姓氏。这种技术通常被称为非破坏性突变。记录代表的不是人在一段时间内的状态,而是人在给定时间的状态。
为了帮助实现这种编程风格,记录允许使用一种新的表达式;with-表达式:
with-表达式使用对象初始值设定项语法来声明新对象与旧对象的不同之处。您可以指定多个属性。
记录隐式定义了受保护的“复制构造函数”-该构造函数接受现有记录对象并将其逐个字段复制到新的记录对象中:
With表达式导致调用复制构造函数,然后在顶部应用对象初始值设定项以相应地更改属性。
如果您不喜欢生成的复制构造函数的默认行为,您可以定义自己的行为,这将由with表达式选择。
所有对象都从Object类继承一个虚拟的equals(Object)方法。当两个参数都非空时,这用作Object.Equals(Object,Object)静态方法的基础。
结构将其覆盖为具有“基于值的相等”,通过递归调用结构的每个字段上的equals来比较它们。唱片也是这样做的。
这意味着根据它们的“价值性”,两个记录对象可以彼此相等,而不是相同的对象。例如,如果我们再次修改修改后的人员的姓氏:
我们现在的ReferenceEquals(Person,OriginalPerson)=false(它们不是同一个对象),但是equals(Person,OriginalPerson)=true(它们具有相同的值)。
如果您不喜欢生成的equals重写的默认逐字段比较行为,您可以编写自己的行为。您只需要小心理解基于值的相等在记录中是如何工作的,特别是当涉及到继承时,我们将在下面讨论这一点。
除了基于值的equals之外,还有一个基于值的GetHashCode()覆盖。
绝大多数记录都是不可变的,只有init-only的公共属性可以通过with-expression进行非破坏性修改。为了针对这种常见情况进行优化,记录会更改表单字符串FirstName的简单成员声明的默认值。与其他类和结构声明中的隐式私有字段不同,在记录中,这被认为是公共的、仅限初始化的自动属性的简写!因此,宣言:
公共数据类Person{public String FirstName{get;init;}公共String LastName{get;init;}}。
我们认为这使得声明变得漂亮而清晰。如果您确实需要私有字段,只需显式添加private修饰符即可:
有时,对记录采用更多的位置方法很有用,因为它的内容是通过构造函数参数给出的,并且可以通过位置解构来提取。
完全可以在记录中指定您自己的构造函数和解构函数:
公共数据类Person{String FirstName;String LastName;public Person(String FirstName,String LastName)=&>;(FirstName,LastName)=(FirstName,LastName)=(FirstName,LastName);public void deconstruct(out String FirstName,out String LastName)=>;(FirstName,LastName)=(FirstName,LastName);}
但是有一个更短的语法来表达完全相同的内容(参数名模大小写):
这将声明公共的仅初始化自动属性以及构造函数和解构函数,以便您可以编写:
var Person=新的人(";Scott&34;,";Hunter&34;);//位置结构var(f,l)=Person;//位置解构。
如果您不喜欢生成的自动属性,您可以定义您自己的同名属性,生成的构造函数和解构函数将只使用该属性。
记录的基于值的语义与可变状态不能很好地结合。想象一下将一个记录对象放入字典中。再次找到它取决于EQUALS和(有时)GethashCode。但是如果记录改变了它的状态,它也会改变它等于的内容!我们可能再也找不到了!在哈希表实现中,它甚至可能损坏数据结构,因为放置是基于它“到达时”的哈希代码!
在记录中可能有一些可变状态的有效高级用法,特别是用于缓存。但是,覆盖默认行为以忽略此类状态所涉及的手动工作可能会相当繁重。
基于值的平等和非破坏性突变在与继承相结合时是出了名的具有挑战性。让我们将派生记录类Study添加到我们的运行示例中:
公共数据类Person{String FirstName;String LastName;}公共数据类学生:Person{int ID;}
让我们从-expression示例开始,实际创建一个Student,但将其存储在Person变量中:
Person Person=新学生{FirstName=";Scott";,LastName=";Hunter";,ID=GetNewId()};其他Person=具有{LastName=";Hanselman";};
在最后一行的with-expression处,编译器并不知道该Person实际上包含了一名学生。然而,如果新的人实际上不是一个学生对象,并且与第一个复制过来的人具有相同的ID,那么它就不是一个正确的副本。
C#实现了这一点。记录有一个隐藏的虚拟方法,委托它“克隆”整个对象。每个派生记录类型重写此方法以调用该类型的复制构造函数,并将派生记录的复制构造函数链到基记录的复制构造函数。with-expression只调用隐藏的“clone”方法,并将对象初始值设定项应用于结果。
与With-Expression支持类似,基于值的等式也必须是“虚拟的”,因为学生需要比较所有的学生字段,即使在比较时静态已知的类型是像Person这样的基类型。这很容易通过重写已经有效的equals方法来实现。
然而,平等还有一个额外的挑战:如果你比较两种不同的人会怎么样?我们真的不能让他们中的一个来决定应用哪一个相等:相等应该是对称的,所以无论两个对象中哪一个先出现,结果都应该是相同的。换句话说,他们必须就适用的平等达成一致!
人员Person1=新人员{FirstName=";Scott";,LastName=";Hunter";};人员Pers2=新学生{FirstName=";Scott";,LastName=";Hunter";,ID=GetNewId()};
这两个物体是否相等?角色1可能是这样认为的,因为角色2拥有所有人的权利,但是角色2却不敢苟同!我们需要确保他们都同意他们是不同的对象。
同样,C#会自动为您处理此问题。它的实现方式是记录有一个名为EqualityContract的虚拟受保护属性。每个派生记录都会覆盖它,为了比较相等,这两个对象必须具有相同的EqualityContract。
这不仅会让语言初学者不堪重负,而且会使代码变得混乱,并增加缩进级别。
在C#9.0中,您可以选择在顶层编写主程序:
允许任何语句。该程序必须在使用之后、文件中的任何类型或名称空间声明之前进行,并且您只能在一个文件中执行此操作,就像您现在只能有一个main方法一样。
如果要返回状态代码,可以这样做。如果你想等事情,你可以那样做。如果您想访问命令行参数,可以使用args作为“神奇”参数。
局部函数是语句的一种形式,在顶级程序中也是允许的。从顶层语句节之外的任何位置调用它们都是错误的。
C#9.0中添加了几种新的模式。让我们在模式匹配教程中的这段代码片段的上下文中查看它们:
公共静态小数计算通行费(目标车辆)=>;车辆开关{.。DeliveryTruck t当t.GrossWeightClass>;5000=>;10.00m+5.00m时,DeliveryTruck t当t.GrossWeightClass<;3000=>;10.00m-2.00m时,DeliveryTruck_=>;10.00m,_=>;抛出新的ArgumentException(";不是已知的车辆类型";,名称为(Vehicle))};
目前,类型模式需要在类型匹配时声明标识符-即使该标识符是Discard_,就像上面的DeliveryTruck_那样。但现在您只需编写类型:
C#9.0引入了与关系运算符<;、<;=等相对应的模式。因此,您现在可以将上述模式的DeliveryTruck部分编写为嵌套开关表达式:
DeliveryTruck t当t.GrossWeightClass交换机{>;5000=>;10.00m+5.00m,<;3000=>;10.00m-2.00m,_=>;10.00m,},
最后,您可以将模式与逻辑运算符AND、OR和NOT组合为单词,以避免与表达式中使用的运算符混淆。例如,上面嵌套开关的情况可以按升序排列,如下所示:
DeliveryTruck t当t.GrossWeightClass开关{<;3000=>;10.00M-2.00M,<;=3000且<;=5000=>;10.00M,>;5000=>;10.00M+5.00M,},
中间的情况使用AND组合两个关系模式,形成一个表示间隔的模式。
NOT模式的常见用法是将其应用于NULL常量模式,如NOT NULL。例如,我们可以根据未知案例是否为空来划分未知案例的处理:
NOT NULL=&gT;抛出新的ArgumentException($";不是已知的车辆类型:{Vehicle}";,nameof(Vehicle)),NULL=>;抛出新的ArgumentNullException(nameof(Vehicle))
Also Not在包含is表达式的if条件中会很方便,而不是使用笨拙的双圆括号:
“目标类型”是我们用来表示表达式从其使用位置的上下文中获取其类型时使用的术语。例如,null和lambda表达式总是目标类型的。
在C#9.0中,一些以前不是目标类型的表达式变得能够由它们的上下文来引导。
C#中的新表达式始终要求指定类型(隐式类型数组表达式除外)。现在,如果有明确的类型为表达式赋值,则可以省略该类型。
有时是有条件的??AND?:表达式在分支之间没有明显的共享类型。这样的情况现在失败了,但是如果有两个分支都转换成的目标类型,C#9.0将允许它们:
人人=学生??客户;//共享库类型?result=b?0:空;//可以为空的值类型。
有时,表示派生类中的方法重写具有比基类型中的声明更具体的返回类型是很有用的。C#9.0允许:
抽象类Animal{public抽象Food GetFood();.}类Tiger:Animal{public Override Meat GetFood()=>;.;}
查看C#9.0即将推出的全套特性并跟踪其完成情况的最佳位置是Roslyn(C#/VB编译器)GitHub Repo上的语言特性状态。