微软正在推动C#9.0的开发,C#9.0将成为.NET5开发平台的一部分,估计于11月发布。微软.NET团队C#首席设计师MadsTorgersen表明,C#9.0已初具规模,本文就共享下该言语下一版别中添
加的一些主要功能。
C#的每个新版别都力求提升通用编程方面的清晰度与简略性,C#9.0也不破例,尤其重视支撑数据形状的简洁与不可变表明。下面,咱们就来详细介绍!
仅可初始化的特点
方针的初始化器十分了不得。它们为客户端创立方针供给了一种十分灵活且易于阅览的格局,并且特别合适嵌套方针的创立,咱们能够经过嵌套方针一次性创立整个方针树。下面是一个简略的比方:
newPerson{FirstName="Scott",LastName="Hunter"}
方针初始化器还能够让程序员免于编写很多类型的结构样板代码,他们只需编写一些特点即可!
publicclassPerson{publicstringFirstName{get;set;}publicstringLastName{get;set;}}
目前的一大限制是,特点有必要是可变的,只要这样方针初始化器才干起作用,由于它们需求首要调用方针的结构函数(在这种状况下调用的是默许的无参结构函数),然后分配给特点设置器。
仅可初始化的特点能够解决这个问题!它们引入了init拜访器。init拜访器是set拜访器的变体,它只能在方针初始化期间调用:
publicclassPerson{publicstringFirstName{get;init;}publicstringLastName{get;init;}}
在这种声明下,上述客户端代码依然合法,可是后续假如你想为FirstName和LastName特点赋值就会犯错。
初始化拜访器和只读字段
由于init拜访器只能在初始化期间被调用,所以它们能够修正地点类的只读字段,就像结构函数相同。
publicclassPerson{privatereadonlystringfirstName;privatereadonlystringlastName;
publicstringFirstName{get=>firstName;init=>firstName=(value??thrownewArgumentNullException(nameof(FirstName)));}publicstringLastName{get=>lastName;init=>
lastName=(value??thrownewArgumentNullException(nameof(LastName)));}}
记载
假如你想保持某个特点不变,那么仅可初始化的特点十分有用。假如你期望整个方针都不可变,并且期望其行为宛如一个值,那么就应该考虑将其声明为记载:
publicdataclassPerson{publicstringFirstName{get;init;}publicstringLastName{get;init;}}
上述类声明中的data关键字表明这是一个记载,因而它具备了其他一些相似于值的行为,后面咱们将深入评论。一般而言,咱们更应该将记载视为“值”(数据),而非方针。它们不具备可变的封装
状况。相反,你能够经过创立表明新状况的新记载来表明随着时刻发作的变化。记载不是由标识确认,而是由其内容确认。
With表达式
处理不可变数据时,一种常见的方式是运用现有的值创立新值以表明新状况。例如,假如想修正或人的姓氏,那么咱们会用一个新方针来表明,这个方针除了姓氏之外和旧方针完全相同。通常咱们称
该技能为非破坏性修正。记载代表的不是某段时刻的某个人,而是给定时刻点上这个人的状况。
为了帮助大家习惯这种编程风格,记载答应运用一种新的表达办法:with表达式:
varotherPerson=personwith{LastName="Hanselman"};
with表达式运用方针初始化的语法来说明新方针与旧方针之间的区别。你能够指定多个特点。
记载隐式地界说了一个protected“仿制结构函数”,这种结构函数运用现有的记载方针,将字段逐个仿制到新的记载方针中:
protectedPerson(Personoriginal){/*copyallthefields*/}//generated
with表达式会调用仿制结构函数,然后在其上应用方针初始化器,以相应地更改特点。
假如你不喜欢主动生成的仿制结构函数,那么也能够自己界说,with表达式就会调用自界说的仿制结构函数。
依据值的持平性
一切方针都会从object类承继一个虚的Equals(object)办法。在调用静态办法Object.Equals(object,object)且两个参数均不为null时,该Equals(object)就会被调用。
结构体能够重载这个办法,取得“依据值的持平性”,即递归调用Equals来比较结构的每个字段。记载也相同。
这意味着,假如两个记载方针的值共同,则二者持平,但两者不一定是同一方针。例如,假如咱们再次修正前面那个人的姓氏:
varoriginalPerson=otherPersonwith{LastName="Hunter"};
现在,ReferenceEquals(person,originalPerson)=false(它们不是同一个方针),但Equals(person,originalPerson)=true(它们拥有相同的值)。
假如你不喜欢主动生成的Equals覆盖默许的逐字段比较的行为,则能够编写自己的Equals重载。你只需求保证你理解依据值的持平性在记载中的作业原理,尤其是在涉及承继的状况下,详细的内容我
们稍后再做介绍。
除了依据值的Equals之外,还有一个依据值的GetHashCode()重载办法。
数据成员
在绝大多数状况下,记载都是不可变的,它们的仅可初始化的特点是揭露的,能够经过with表达式进行非破坏性修正。为了优化这种最常见的状况,咱们改变了记载中相似于stringFirstName这种成
员声明的默许意义。在其他类和结构声明中,这种声明表明私有字段,但在记载中,这相当于揭露的、仅可初始化的主动特点!因而,如下声明:
publicdataclassPerson{stringFirstName;stringLastName;}
与之前提到过的下述声明完全相同:
publicdataclassPerson{publicstringFirstName{get;init;}publicstringLastName{get;init;}}
咱们以为这种办法能够让记载更加美丽而清晰。假如你需求私有字段,则能够清晰增加private修饰符:
privatestringfirstName;
方位记载
有时,用参数方位来声明记载会很有用,内容能够依据结构函数参数的方位来指定,并且能够经过方位解构来提取。
你完全能够在记载中指定自己的结构函数和析构函数:
publicdataclassPerson{stringFirstName;stringLastName;publicPerson(stringfirstName,stringlastName)=>(FirstName,LastName)=(firstName,lastName);publicvoid
Deconstruct(outstringfirstName,outstringlastName)=>(firstName,lastName)=(FirstName,LastName);}
可是,咱们能够用更短的语法表达完全相同的内容(运用成员变量的大小写办法来命名参数):
publicdataclassPerson(stringFirstName,stringLastName);
上述声明晰仅可初始化的揭露的主动特点以及结构函数和析构函数,因而你能够这样写:
varperson=newPerson("Scott","Hunter");//positionalconstructionvar(f,l)=person;//positionaldeconstruction
假如你不喜欢生成的主动特点,则能够界说自己的同名特点,这样生成的结构函数和析构函数就会主动运用自己界说的特点。
记载和修正
记载的语义是依据值的,因而在可变的状况中无法很好地运用。幻想一下,假如咱们将记载方针放入字典,那么就只能经过Equals和GethashCode找到了。可是,假如记载更改了状况,那么在判别相
等时它代表的值也会发作改变!或许咱们就找不到它了!在哈希表的完成中,这个性质乃至或许破坏数据结构,由于数据的寄存方位是依据它“抵达”哈希表时的哈希值决议的!
并且,记载也或许有一些运用内部可变状况的高档办法,这些办法完全是合理的,例如缓存。可是能够考虑经过手工重载默许的行为来忽略这些状况。
with表达式与承继
众所周知,考虑承继时依据值的持平性和非破坏性修正是一个难题。下面咱们在示例中增加一个承继的记载类Student:
publicdataclassPerson{stringFirstName;stringLastName;}publicdataclassStudent:Person{intID;}
在如下with表达式的示例中,咱们实践创立一个Student,然后将其存储到Person变量中:
Personperson=newStudent{FirstName="Scott",LastName="Hunter",ID=GetNewId()};otherPerson=personwith{LastName="Hanselman"};
在最后一行的with表达式中,编译器并不知道person实践上包括一个Student。并且,即便otherPerson不是Student方针,它也不是合法的副本,由于它包括了与第一个方针相同的ID特点。
C#解决了这个问题。记载有一个躲藏的虚办法,能够保证“克隆”整个方针。每个承继的记载类型都会经过重载这个办法来调用该类型的仿制结构函数,而承继记载的仿制结构函数会调用基类的仿制
结构函数。with表达式只需调用这个躲藏“clone”办法,然后在结果上应用方针初始化器即可。
依据值的持平性与承继
与with表达式的支撑相似,依据值的持平性也有必要是“虚的”,即两个Student方针比较时需求比较一切字段,即便在比较时,能够静态地得知类型是基类,比方Person。这一点经过重写已经是虚方
法的Equals办法能够轻松完成。
然而,持平性还有另外一个难题:假如需求比较两个不同类型的Person怎么办?咱们不能简略地选择其中一个来决议是否持平:持平性应该是对称的,因而不管两个方针中的哪个首要呈现,结果都应
该相同。换句话说,二者之间有必要就持平性达成共同!
咱们来举例说明这个问题:
Personperson1=newPerson{FirstName="Scott",LastName="Hunter"};Personperson2=newStudent{FirstName="Scott",LastName="Hunter",ID=GetNewId()};
这两个方针互相持平吗?person1或许会以为持平,由于person2拥有Person的一切字段,但person2或许会有不同的观点!咱们需求保证二者都认同它们是不同的方针。
C#能够主动为你解决这个问题。详细的完成办法是:记载拥有一个名为EqualityContract的受维护虚特点。每个承继的记载都会重载这个特点,并且为了比较持平,两个方针有必要具有相同的
EqualityContract。
顶层程序
运用C#编写一个简略的程序需求很多的样板代码:
usingSystem;classProgram{staticvoidMain(){Console.WriteLine("HelloWorld!");}}
这不仅对初学者来说难度太高,并且代码紊乱,缩进等级也太多。
在C#9.0中,你只需编写顶层的主程序:
usingSystem;
Console.WriteLine("HelloWorld!");
任何句子都能够。程序有必要位于using之后,文件中的任何类型或称号空间声明之前,并且只能在一个文件中,就像只要一个Main办法相同。
假如你想回来状况代码,则能够运用这种写法。假如你想await,那么也能够这么写。此外,假如你想拜访命令行参数,则args可作为“魔术”参数运用。
局部函数是句子的一种方式,并且也能够在顶层程序中运用。在顶层句子之外的任何地方调用局部函数都会报错。
改善后的方式匹配
C#9.0中增加了几种新的方式。下面咱们经过如下方式匹配教程的代码片段来看看这些新方式:
publicstaticdecimalCalculateToll(objectvehicle)=>vehicleswitch{...
DeliveryTrucktwhent.GrossWeightClass>5000=>10.00m+5.00m,DeliveryTrucktwhent.GrossWeightClass<3000=>10.00m-2.00m,DeliveryTruck_=>10.00m,
_=>thrownewArgumentException("Notaknownvehicletype",nameof(vehicle))};
简略类型方式
当前,类型方式需求在类型匹配时声明一个标识符,即便该标识符是表明放弃的_也能够,如上面的DeliveryTruck_。而如今你能够像下面这样编写类型:
DeliveryTruck=>10.00m,
联系方式
C#9.0中引入了与联系运算符<、<=等相对应的方式。因而,你能够将上述方式的DeliveryTruck写成嵌套的switch表达式:
DeliveryTrucktwhent.GrossWeightClassswitch{>5000=>10.00m+5.00m,<3000=>10.00m-2.00m,_=>10.00m,},
此处的>5000和<3000是联系方式。
逻辑方式
最后,你还能够将方式与逻辑运算符(and、or和not)组合在一起,它们以英文单词的方式呈现,以避免与表达式中运用的运算符混淆。例如,上述嵌套的switch表达式能够按照升序写成下面这样:
DeliveryTrucktwhent.GrossWeightClassswitch{<3000=>10.00m-2.00m,>=3000and<=5000=>10.00m,>5000=>10.00m+5.00m,},
中间一行经过and将两个联系方式组合到一起,形成了表明间隔的方式。
not方式的常见用法也可应用于null常量方式,比方notnull。例如,咱们能够依据是否为null来拆分未知状况的处理办法:
notnull=>thrownewArgumentException($"Notaknownvehicletype:{vehicle}",nameof(vehicle)),null=>thrownewArgumentNullException(nameof(vehicle))
此外,假如if条件中包括is表达式,那么运用not也很方便,能够避免笨拙的双括号:
if(!(eisCustomer)){...}
你能够这样写:
if(eisnotCustomer){...}
改善后的方针类型揣度
“方针类型揣度”指的是表达式从地点的上下文中获取类型。例如,null和lambda表达式一直是方针类型揣度。
在C#9.0中,有些以前不是方针类型揣度的表达式也能够经过上下文来判别类型。
支撑方针类型揣度的new表达式
C#中的new表达式一直要求指定类型(隐式类型的数组表达式除外)。现在,假如有清晰的类型能够分配给表达式,则能够省去指定类型。
Pointp=new(3,5);
方针类型的??与?:
有时,条件判别表达式中??与?:的各个分支之间并不是很明显的同一种类型。现在这种状况会犯错,但在C#9.0中,假如两个分支都能够转换为方针类型,就没有问题:
Personperson=student??customer;//Sharedbasetypeint?result=b?0:null;//nullablevaluetype
协变的回来值
有时,咱们需求表明出承继类中重载的某个办法的回来类型要比基类中的类型更详细。C#9.0答应以下写法:
abstractclassAnimal{publicabstractFoodGetFood();...}classTiger:Animal{publicoverrideMeatGetFood()=>...;}