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. I - Union 2019ccpc女生赛

    I - Union 这是2019女生赛最难的一个题目,但是现在去写,我觉得没有想象之中的那么难. 把这个题目分成几个部分来考虑. 假设给你k个数,让你分成三个集合,满足这四个条件,且不需要考虑时间和空 ...

  2. idea设置配置提示模板

    File-->Settings-->LIve Templates-->+-->Template Group(模板名称)-->Live Template

  3. Spring官网阅读(七)容器的扩展点(二)FactoryBean

    在上篇文章中我们已经对容器的第一个扩展点(BeanFactoryPostProcessor)做了一系列的介绍.其中主要介绍了Spring容器中BeanFactoryPostProcessor的执行流程 ...

  4. Java语言简介、基础组成、封装、继承、多态、抽象类、内部类、接口

    目录 Java简介 Java语言基础组成 面向对象 对象 封装 构造函数 this关键字 static(静态关键字) 主函数 静态什么时候用呢? 面向对象(数组工具对象建立) 设计模式 继承 成员变量 ...

  5. Android将库导入到build.gradle

    如图

  6. DNSlog注入学习

    之前一直有看到过DNSlog这个字眼,但一直没有好好去了解一下,最近又接触到了刚好来深入学习下 0x01 什么是DNSlog 我们都知道DNS就是将域名解析为ip,用户在浏览器上输入一个域名A.com ...

  7. springmvc 文件上传异步处理

    springmvc3提供了文件上传异步处理功能,当文件上传时,controller不需要一直等到文件上传成功后再返回视图,而是先返回到servlet容器,待异步处理的线程完成后转向指定视图! 首先要在 ...

  8. vs2015 cppunit配置及使用

    目录 第一步 第二步 第三步 编译生成lib库 使用 calculator类测试 代码部分 第一步 下载源代码 http://sourceforge.net/projects/cppunit/file ...

  9. centos7 git下载速度慢

    nslookup命令 yum -y install bind-utils [root@iZ1i4qd6oynml0Z ~]# nslookup github.global.ssl.fastly.Net ...

  10. utf8mb4复杂昵称问题

    wechat_ling wl_channel_consumer nickname wl_consumer nickname alter table wl_channel_consumer modify ...