C# 9.0正在形成,我想分享我们对添加到该语言下个版本的一些主要功能的看法。对于每个新版本的 C#,我们努力使常见的编码方案更加清晰和简单,C# 9.0 也不例外。这次的一个特别重点是支持数据形状的简洁和不可变表示。

  让我们潜入吧!

1 仅可初始化的属性

  对象初始化器是非常好用的。它们为类型实例化提供了一种非常灵活且可读的格式来创建对象,尤其是对于一次创建特别大的嵌套对象来说。下面是一个简单的例子:

new Person
{
FirstName = "Scott",
LastName = "Hunter"
}

  对象初始化也使用户不必编写大量构造函数,要做的就是编写一些属性!

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

  今天,一个很大的限制是,属性必须是可修改的,对象初始化器是这样工作的:首先调用对象的构造函数(默认为无参的构造函数),然后分配给属性设置器(property setter)。

  仅可初始化属性修改了这一点!它们引入了一个 init 访问器,该访问器是set访问器的变体,只能在对象初始化期间调用:

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

  使用此声明,除了初始化外,之后任何后续赋值给 FirstName 和 LastName 属性都是一个错误。

  因为init访问器只能在初始化期间访问,因此他们允许修改封闭类型中的只读字段,就像在构造函数中那样:

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

2 记录

  如果要使单个属性不可变,则仅可初始化属性非常适合。如果希望整个对象不可变且像值类型一样,则应考虑将其声明为记录:

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

  类声明中的data关键字将其标记为记录。这赋予它几个类似价值类型的行为,我们将在下面深入探讨这些行为。一般来说,记录更被视为"值"(纯数据), 而不是作为对象。您可以通过创建新记录表示新状态来表示随时间的变化。它们不是由标识定义,而是由其内容定义。

2.1 With表达式

  使用不可变数据时,一种常见模式是从现有值创建新值以表示新状态。例如,如果我们更改LastName,我们会将其表示为一个新对象,该对象是旧对象的副本,但LastName不同。这种技术通常被称为非破坏性修改。记录这种特性表示的是Person在给定时间的状态。

  为了适应这种编程风格,记录允许一种新的表达式——with:

var otherPerson = person with{LastName="Hanselman"};

  with表达式使用对象初始化器语法来说明新对象与旧对象的不同内容。您可以指定多个属性。

  记录隐式定义一个受保护的"复制构造函数"-一个构造函数,它获取现有记录对象,并逐个将其字段复制到新的对象:

protected Person(Person original){/* copy all the fields */}// generated

  with 表达式会导致调用复制构造函数,然后在上面应用对象初始化器以相应地更改属性。

  如果您不喜欢生成的复制构造函数的默认行为,则可以改为定义自己的行为,该行为将由with表达式选取。

2.2 基于值的相等性

  所有对象都从Object继承 Equals(object)。结构将其重写为具有"基于价值的相等性",通过递归地调用Equals来比较结构的每个字段。记录也执行相同的操作。这意味着,根据其"值",两个记录对象可以彼此相等,而不必是同一对象。例如:

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

  现在 ReferenceEquals(person, originalPerson) = false(这两个不是一个对象)但是Equals(person, originalPerson) = true (他们有相同的值)。

  如果您不喜欢生成的 Equals 重写的默认逐字段比较行为,则可以改为编写自己的字段比较行为。你只需要小心,你了解基于值的相等在记录中是如何工作的,特别是当涉及继承时。

  除了重写Equals 外,还有 GetHashCode()。

2.3 数据成员

  记录绝大多数都是不可变的,只有只读初始化器可以通过with表达式进行非破坏性修改。为了针对这种常见情况进行优化,记录在声明时会更改string FirstName这类成员声明的行为。与其他类和结构声明中的隐式private字段不同,在记录中,这被视为public的、仅可初始化的自动属性的缩写!因此:

public data classPerson
{
string FirstName;
string LastName;
}

  与

public data classPerson
{
public string FirstName{get; init;}
public string LastName{get; init;}
}

  是相同的。

  我们认为这有助于做出漂亮而清晰的记录声明。如果您真的需要私有字段,只需显式地添加private修饰符:

private string firstName;

2.4 基于位置的记录

  有时,对记录采用更为位置化的方法是有用的,在这种方法中,记录的内容通过构造函数参数的位置给出,并且可以通过解构函数来提取。

  可以在记录中指定自己的构造函数和解构函数:

public data classPerson
{
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);
}

  上面代码可以简写为:

public data class Person(string FirstName,string LastName);

  这将声明public的仅初始化的自动属性以及构造函数和解构函数,以便您可以编写:

var person =new Person("Scott","Hunter");// positional construction
var(f, l)= person; // positional deconstruction

  如果您不喜欢生成的自动属性,则可以改为定义自己的同名属性,生成的构造函数和解构函数将使用该属性。

2.5 记录的改变引发的问题

  想象一下,将记录对象放入字典中。再次找到它取决于 Equal 和GetHashCode。如果记录改变其状态,它也会改变它等于什么!我们可能再也找不到了!在哈希表实现中,它甚至可能损坏数据结构,因为定位基于的是"到达哈希表时"的哈希值!

  虽然可以通过重写一些内部方法来改变这种默认的行为,但其工作量也是相当巨大的。

2.6 with表达式与继承

public data class Person{string FirstName;string LastName;}
public data class Student:Person{int ID;}
Person person =new Student{FirstName="Scott",LastName="Hunter", ID =GetNewId()};
otherPerson = person with{LastName="Hanselman"};

  在最后一行上使用with表达式时,编译器不知道person实际上包含了一个Student。而且,即使otherPerson实际上不是"Student"对象,它也不是一个正确的副本,该对象与复制的第一个对象具有相同的ID。

  记录有一个隐藏的虚方法,它委托"克隆"整个对象。每个派生记录类型都重写此方法以调用该类型的复制构造函数,以及派生链上的复制构造函数直到基类记录的复制构造函数。with表达式只需调用隐藏的"克隆"方法,并将对象初始化器应用于结果。

2.7 值相等与继承

  与with表达式的实现类似,基于值的相等性也必须是"虚拟"的,即Student需要比较所有字段,即使比较时能够得知类型是基类型Person。这是很容易通过重写已经虚拟的Equals方法实现的。

  但是,相等还有一个挑战:如果比较两种不同的Person,该怎么办?我们不能让其中一个决定是否相等:相等应该是对称的,所以无论两个对象中哪个是第一个,结果都应该是相同的。换句话说,他们必须就适用的相等达成一致!

  说明问题的示例:

Person person1 =new Person{FirstName="Scott",LastName="Hunter"};
Person person2 =new Student{FirstName="Scott",LastName="Hunter", ID =GetNewId()};

  这两个对象彼此相等吗?person1可能会这样认为,因为person2有所有的Person的构造,但person2会认为与person1不同!我们需要确保他们都同意他们是不同的对象。

  C# 会自动为您处理。它的实现方式是每个记录都有一个"EqualityContract"的虚拟受保护属性。每个派生记录都会重写它,为了比较相等,两个对象必须具有相同的EqualityContract。

3 简化顶级程序

  之前我们这样写代码:

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

  现在您可以选择在顶层编写主程序:

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

  支持任何语句,但必须在using之后以及文件中的任何类型或命名空间声明之前,并且只能在一个文件中执行此操作,就像目前只能有一个Main方法一样。如果要返回状态代码,可以执行此操作。如果你想await,你可以这样做。如果要访问命令行参数,可以访问args参数。

  局部函数是语句的一种形式,在顶级程序中也允许使用。从顶级语句部分以外的任何位置调用它们都是错误的。

4 改进模式匹配

  在 C# 9.0 中添加了几种新类型的模式。例如:

public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
...
DeliveryTruck t when t.GrossWeightClass > => 10.00m + 5.00m,
DeliveryTruck t when t.GrossWeightClass < => 10.00m - 2.00m,
DeliveryTruck _ => 10.00m,
_ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
};

4.1 简单类型模式

  目前,类型模式需要在类型匹配时声明一个标识符,即使该标识符是一个_,比如 DeliveryTruck  _。新语法不用了,可以简写为:

DeliveryTruck => 10.00m,

4.2 关系模式

  C#9.0引入了对应于关系运算符<、<=等的模式。因此,新语法可以这样写:

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

4.3 逻辑模式

  最后,可以将模式与逻辑运算(and 、or、not)符组合起来,并将其拼写为单词,以避免与表达式中使用的运算符混淆。例如:

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

  not的常见用法是将其应用于判空。例如:

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

  还有,if (!(e is Customer)) { ... }在新语法中,可以写为if (e is not Customer) { ... }

5 目标类型

  "Target typing"是当表达式从使用位置的上下文中获取其类型时,我们使用的术语。C# 9.0支持新的类型推断。

5.1 new

  新语法中,如果是明确的类型,则在使用new时,可以不声明类型了。比如:

Point p = new (, );

5.2 ?? and ?:

  目前,??与?:如果分支之间不是同一类型会报错。新语法下,如果两个分支都可以转换为目标类型则是允许的:

Person person = student ?? customer; // Shared base type
int? result = b ? : null; // nullable value type

6 改进协变

  有时,派生类中的方法返还比基类中的声明更具体的类型是很有用的。C# 9.0 允许:

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

  此外,还要很多新的改进,让我们拭目以待吧。

原文链接

https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/?utm_source=vs_developer_news&utm_medium=referral

【译】Welcome to C# 9.0的更多相关文章

  1. [译]ASP.NET Core 2.0 系列文章目录

    基础篇 [译]ASP.NET Core 2.0 中间件 [译]ASP.NET Core 2.0 带初始参数的中间件 [译]ASP.NET Core 2.0 依赖注入 [译]ASP.NET Core 2 ...

  2. [译]ASP.NET Core 2.0 中间件

    问题 如何创建一个最简单的ASP.NET Core中间件? 答案 使用VS创建一个ASP.NET Core 2.0的空项目,注意Startup.cs中的Configure()方法: public vo ...

  3. [译]ASP.NET Core 2.0 带初始参数的中间件

    问题 如何在ASP.NET Core 2.0向中间件传入初始参数? 答案 在一个空项目中,创建一个POCO(Plain Old CLR Object)来保存中间件所需的参数: public class ...

  4. [译]ASP.NET Core 2.0 全局配置项

    问题 如何在 ASP.NET Core 2.0 应用程序中读取全局配置项? 答案 首先新建一个空项目,并添加两个配置文件: 1. appsettings.json { "Section1&q ...

  5. [译]ASP.NET Core 2.0 机密配置项

    问题 如何在ASP.NET Core 2.0中保存机密配置项(不用将其暴露给源代码管理器)? 答案 创建一个ASP.NET Core 2.0空项目,在项目节点上点击右键,并点击菜单项 - 管理用户机密 ...

  6. [译]ASP.NET Core 2.0 会话状态

    问题 如何在ASP.NET Core 2.0中存储会话状态? 答案 创建一个空项目,修改Startup类的ConfigureServices()方法,添加会话状态服务和它后台的存储服务: public ...

  7. [译]ASP.NET Core 2.0 本地文件操作

    问题 如何在ASP.NET Core 2.0中受限地访问本地目录和文件信息? 答案 新建一个空项目,修改Startup类,添加访问本地文件所需的服务: public void ConfigureSer ...

  8. [译]ASP.NET Core 2.0 网址重定向

    问题 如何在ASP.NET Core 2.0中实现网址重定向? 答案 新建一个空项目,在Startup.cs文件中,配置RewriteOptions参数并添加网址重定向中间件(UseRewriter) ...

  9. [译]ASP.NET Core 2.0 路由引擎

    问题 ASP.NET Core 2.0的路由引擎是如何工作的? 答案 创建一个空项目,为Startup类添加MVC服务和请求中间件: public void ConfigureServices(ISe ...

  10. [译]ASP.NET Core 2.0 路由引擎之网址生成

    问题 如何在ASP.NET Core 2.0中由路由引擎来生成网址? 答案 新建一个空项目,修改Startup.cs文件,添加MVC服务和中间件: public void ConfigureServi ...

随机推荐

  1. Python爬虫(一)爬百度贴吧

    简单的GET请求: # python2 import urllib2 response = urllib2.urlopen('http://www.baidu.com') html = respons ...

  2. 集成学习基础知识总结-Bagging-Boosting

    理论 在概率近似正确((probably approximately correct)学习框架下.一个概念是强可学习的充分必要条件是这个概念是弱可学习(仅比随机猜测稍好). 要求 个体学习器要好而不同 ...

  3. js怎么删除数组元素,有哪些删除元素方法

    JavaScript删除元素方法 1.根据索引删除数组元素 delete 数组[索引]   *注意delete 后面是空格 var arr1=["aa","bb" ...

  4. CSS设置table样式

    \(\color{purple}{表格是个很重要的东西,让我们来美化一下吧!}\) table{ width:290px;height:300px; border:1px solid black;/* ...

  5. Scrapy模块使用出错,出现builtins.ImportError: DLL load failed: 找不到指定的程序

    问题描述:初次学习scrapy,使用scrapy官方文档创建爬虫项目出错, 出现builtins.ImportError: DLL load failed: 找不到指定的程序, ImportError ...

  6. python语法学习第一天--变量、运算符、数据类型

    变量:计算机中的一块内存,给变量赋值意味着将值存入内存中 python中变量不用类型声明(根据赋的值决定类型),但使用时(创建时)必须赋值(=赋值). 多个变量的赋值: ①a=b=c=1; ②a,b, ...

  7. [LA7139 Rotation(2014 shanghai onsite)]二维树状数组

    题意:有一个n*m的矩形,一辆车从左上角出发,沿一条路径走,路径是由矩形上每个单元格的边构成的,最后回到左上角,求车子在每个格子转过圈数的平方和. 思路:假设需要记录每个格子转的顺时针的圈数(为负表示 ...

  8. iOS中的事件响应链、单例模式、工厂模式、观察者模式

    学习内容 欢迎关注我的iOS学习总结--每天学一点iOS:https://github.com/practiceqian/one-day-one-iOS-summary iOS中事件传递和相应机制 i ...

  9. vue 下拉刷新实现

    [手动实现下拉刷新]可以用vue-pull-refash 插件代替 //下拉刷新 let scroll = this.$ref.scroll // 获取当前要拖拽的元素 let top = scrol ...

  10. python解析excel中图片+提取图片

    解析表格是常用的技术.但是有些表各里面有图片怎么办?我想获得表格里面的图片,值得注意的是,图片没有位置信息,所以最好给图片进行编号,编号代表位置. 下面附上提取表格里面图片的代码.只要输出表格地址,和 ...