翻译自 Mads Torgersen 2020年11月10日的博文《C# 9.0 on the record》 [1],Mads Torgersen 是微软 C# 语言的首席设计师,也是微软 .NET 团队的项目群经理。

C# 9.0 正式发布

正式宣布:C# 9.0 发布了! 早在5月,我就写了一篇关于 C# 9.0 计划的博文 [2],以下是该帖子的更新版本,以匹配我们最终实际交付的产品。

对于 C# 的每一个新版本,我们都在努力让常见编码场景的实现变得更加清晰和简单,C# 9.0 也不例外。这次特别关注的是支持数据模型的简洁和不可变表示。

一、仅初始化属性(Init-only properties)

对象初始化器非常棒。它们为类型的客户端提供了一种非常灵活和易读的格式来创建对象,并且特别适合于嵌套对象的创建,让你可以一次性创建整个对象树。这里有一个简单的例子:

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

对象初始化器还使类型作者不必编写大量的构造函数 —— 他们所要做的就是编写一些属性!

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

目前最大的限制是属性必须是可变的(即可写的),对象初始化器才能工作:它们首先调用对象的构造函数(本例中是默认的无参数构造函数),然后赋值给属性 setter

仅初始化(init-only)属性解决了这个问题!它引入了一个 init 访问器,它是 set 访问器的变体,只能在对象初始化时调用:

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

有了这个声明,上面的客户端代码仍然是合法的,但是随后对 FirstNameLastName 属性的任何赋值都是错误的:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK
person.LastName = "Torgersen"; // ERROR!

因此,仅初始化属性可在初始化完成后保护对象的状态免遭突变。

初始化访问器和只读字段(Init accessors and readonly fields)

因为 init 访问器只能在初始化期间调用,所以允许它们更改封闭类的只读(readonly)字段,就像在构造函数中一样。

public class Person
{
private readonly string firstName = "<unknown>";
private readonly string lastName = "<unknown>"; 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)));
}
}

二、记录(Records)

经典的面向对象编程的核心思想是,对象具有强大的身份并封装了随时间演变的可变状态。 C# 在这方面一直都很出色,但是有时您想要的恰恰相反,而在此时,C# 的默认设置往往会妨碍工作,使事情变得非常麻烦。

如果您发现自己希望整个对象是不可变的,并且行为像一个值,那么您应该考虑将其声明为记录(record)

public record Person
{
public string? FirstName { get; init; }
public string? LastName { get; init; }
}

记录仍然是类,但是 record 关键字赋予了它一些另外的类似于值的行为。 一般来说,记录是根据其内容而不是其标识来定义的。 在这点上,记录更接近于结构体,但是记录仍然是引用类型。

虽然记录是可变的,但它们主要是为更好地支持不可变数据模型而构建的。

with 表达式(With-expressions)

处理不可变数据时,一种常见的模式是从现有值创建新值以表示新状态。例如,如果我们的 person 要更改其 LastName,我们会将其表示为一个新对象,该对象是旧对象的副本,只是有不同的 LastName。这种技巧通常被称之为非破坏性突变(non-destructive mutation)。记录(record)不是代表 person 在一段时间内的 状态,而是代表 person 在给定时间点的 状态。

为了帮助实现这种编程风格,记录(record)允许使用一种新的表达式 —— with 表达式:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };

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

with 表达式的工作原理是将旧对象的完整状态实际地复制到一个新对象中,然后根据对象初始化器对其进行改变。这意味着属性必须具有 initset 访问器才能在 with 表达式中进行更改。

基于值的相等(Value-based equality)

所有对象都从对象类(object)继承一个虚的 Equals(object) 方法。这被用作是当两个参数都是非空(non-null)时,静态方法 Object.Equals(object, object) 的基础。

结构体重写了 Equals(object) 方法,通过递归地在结构体的每一个字段上调用 Equals 来比较结构体的每一个字段,从而实现了“基于值的相等”。记录(record)是一样的。

这意味着,根据它们的“值性(value-ness)”,两个记录(record)对象可以彼此相等,而不是同一个对象。例如,如果我们将被修改 personLastName 改回去:

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

现在我们将得到 ReferenceEquals(person, originalPerson) = false(它们不是同一个对象),但是 Equals(person, originalPerson) = true(它们有相同的值)。除了基于值的 Equals 之外,还有一个基于值的 GetHashCode() 重写。另外,记录实现了 IEquatable<T> 并且重载 ==!= 操作符,因此基于值的行为在所有这些不同的相等机制中表现一致。

值的相等性和可变性并不总是很好地融合在一起。一个问题是,更改值可能导致 GetHashCode 的结果随时间变化,如果对象存储在哈希表中,这是很不幸的!我们不会禁止使用可变记录,但是我们不鼓励它们,除非您充分考虑过后果!

继承(Inheritance)

记录可以从其他记录继承:

public record Student : Person
{
public int ID;
}

with 表达式和值的相等性与记录的继承很好地结合在一起,因为它们考虑了整个运行时对象,而不仅仅是它的静态已知类型。假设我创建了一个 Student,但将其存储在 Person 变量中:

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

with 表达式仍将复制整个对象并保留运行时类型:

var otherStudent = student with { LastName = "Torgersen" };
WriteLine(otherStudent is Student); // true

以相同的方式,值的相等性确保两个对象具有相同的运行时类型,然后比较它们的所有状态:

Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };
WriteLine(student != similarStudent); //true, 因为 ID 不同

位置记录(Positional records)

有时,对记录采用更具位置定位的方法很有用,因为记录的内容是通过构造函数参数指定的,并且可以通过位置解构来提取。完全可以在记录(record)中指定您自己的构造函数和解构函数:

public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string firstName, string lastName)
=> (FirstName, LastName) = (firstName, lastName);
public void Deconstruct(out string firstName, out string lastName)
=> (firstName, lastName) = (FirstName, LastName);
}

但是有一种更简短的语法来表达完全相同的意思(参数名称包装模式modulo casing of parameter names):

public record Person(string FirstName, string LastName);

它声明了公共的仅初始化(init-only)自动属性以及构造函数和解构函数,因此您就可以编写:

var person = new Person("Mads", "Torgersen"); //用位置参数构造(positional construction)
var (f, l) = person; //用位置参数解构(positional deconstruction)

如果不喜欢生成的自动属性,您可以定义自己的同名属性,生成的构造函数和解构函数将只使用您自定义的属性。在这种情况下,该参数在您用于初始化的作用域内。举例来说,假设您希望将 FirstName 设为受保护的属性:

public record Person(string FirstName, string LastName)
{
protected string FirstName { get; init; } = FirstName;
}

位置记录可以像这样调用基构造函数:

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

三、顶级程序(Top-level programs)

译者注:

什么是 Top-level program ? 这是在顶级编写程序的一种更简单的方式:一个更简单的 Program.cs 文件。

用 C# 编写一个简单的程序需要大量的样板代码:

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

这不仅对语言初学者来说是难以承受的,而且还会使代码混乱,增加缩进级别。

在 C# 9.0 中,您可以在顶级编写主程序(main program):

using System;

Console.WriteLine("Hello World!");

允许任何语句。此程序必须在文件中的 using 语句之后,任何类型或命名空间声明之前执行,并且只能在一个文件中执行。就像目前只能有一个 Main 方法一样。

如果您想返回一个状态码,您可以做。如果您想等待(await)事情,您可以做。如果您想访问命令行参数,args 可以作为一个“魔法”参数使用。

using static System.Console;
using System.Threading.Tasks; WriteLine(args[0]);
await Task.Delay(1000);
return 0;

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

四、改进的模式匹配(Improved pattern matching)

C# 9.0 中添加了几种新的模式。让我们从模式匹配教程 [3]的以下代码片段的上下文中来看看它们:

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

简单类型模式(Simple type patterns)

目前,类型模式需要在类型匹配时声明一个标识符 —— 即使该标识符是一个弃元 _,如上面的 DeliveryTruck _ 所示。但现在你只需写下类型就可以了:

DeliveryTruck => 10.00m,

关系模式(Relational patterns)

C# 9.0 引入了与关系运算符 <<= 等相对应的模式。因此,现在可以将上述模式的 DeliveryTruck 部分编写为嵌套的 switch 表达式:

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

这里的 > 5000< 3000 是关系模式。

逻辑模式(Logical patterns)

最后,您可以将模式与逻辑运算符 andornot 组合起来,这些运算符用单词拼写,以避免与表达式中使用的运算符混淆。例如,上面嵌套的 switch 的示例可以按如下升序排列:

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

此例中间的案例使用 and 合并了两个关系模式,形成一个表示区间的模式。

not 模式的一个常见用法是将其应用于 null 常量模式,如 not null。例如,我们可以根据未知实例是否为空来拆分它们的处理:

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

此外,notif 条件中包含 is 表达式时将会很方便,可以取代笨拙的双括号,例如:

if (!(e is Customer)) { ... } // 旧的写法

您可以写成:

if (e is not Customer) { ... } // 新的写法

实际上,在 is not 表达式中,允许您命名 Customer 以供后续使用:

if (e is not Customer c) { throw ... } // 如果此分支,则抛出异常或返回...
var n = c.FirstName; // ... 在这里,c 肯定已赋值

五、目标类型的 new 表达式(Target-typed new expressions)

“目标类型(Target typing)”是我们在表达式从使用位置的上下文中获取其类型时所用的一个术语。例如,nulllambda 表达式始终是目标类型的。

C# 中的 new 表达式总是要求指定类型(隐式类型的数组表达式除外)。在 C# 9.0 中,如果表达式被赋值为一个明确的类型,则可以省略该类型。

Point p = new (3, 5);

当您有很多重复时,例如在数组或对象初始化设定中,这特别地好用:

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) };

六、协变式返回值(Covariant returns)

有时候,这样的表达是有用的 —— 派生类中的方法重写,具有一个比基类型中的声明更具体(更明确)的返回类型。C# 9.0 允许:

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

更多内容……

查看 C# 9.0 全部特性集的最好地方是 “What's new in C# 9.0” 文档页面 [4]

作者 : Mads Torgersen

译者 : 技术译民

出品 : 技术译站

链接 : 英文原文


  1. https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/ C# 9.0 on the record

  2. https://www.cnblogs.com/ittranslator/p/13575059.html 欢迎来到 C# 9.0

  3. https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/pattern-matching pattern matching tutorial

  4. https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-9 What's new in C# 9.0

C# 9.0 正式发布了(C# 9.0 on the record)的更多相关文章

  1. Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)

    千里之行,始于足下.关注公众号[BAT的乌托邦],有Spring技术栈.MyBatis.JVM.中间件等小而美的原创专栏供以免费学习.分享.成长,拒绝浅尝辄止.本文已被 https://www.you ...

  2. AppBox_v2.0完整版免费下载,暨AppBox_v3.0正式发布!

    文章更新: AppBox v6.0中实现子页面和父页面的复杂交互 AppBox 是基于 FineUI 的通用权限管理框架,包括用户管理.职称管理.部门管理.角色管理.角色权限管理等模块. AppBox ...

  3. Socket.IO 1.0 正式发布,快速可靠的实时引擎

    Socket.IO 是目前 Web 领域最火的实时引擎,用于实现基于事件的双向实时的通信.它适用于任何平台,浏览器或设备,专注于可靠性和速度.您可以将数据推送到客户端,并获得实时的计数,日志或图表. ...

  4. Django 1.6.0 正式发布,大幅改进事务处理

    Django 1.6.0 正式发布了,查看官方发行说明. 下载地址:Django-1.6.tar.gz ,也可通过 pip 安装: pip install Django==1.6 要求 Python ...

  5. Apache Spark2.0正式发布

    Apache Spark2.0正式发布 7月26日起Databricks开始提供Apache Spark 2.0的下载,这个版本是基于社区在过去两年的经验总结而成,不但加入了用户喜爱的功能,也修复了之 ...

  6. 【G-BLASTN 1.0正式发布】

    [G-BLASTN 1.0正式发布]G-BLASTN使用GPU来加速NCBI-BLAST里的BLASTN模块,单块GTX780比四核CPU平均快6倍. http://www.comp.hkbu.edu ...

  7. avalon1.0正式发布

    2013年最后的收成:avalon1.0正式发布 大半年前我就说过,MVVM是前端究极的解决方案,因此之后我大多数时间都在折腾avalon,成立了专门的QQ群与感兴趣的一起讨论.感谢第一批吃螃蟹的人, ...

  8. .NET Core 2.0 正式发布信息汇总

    万众瞩目的.NET Core 2.0终于发布了,原定于9.19的dotnetconf大会的发布时间大大提前了1个月,.NET Core 2.0/.NET Standard 2.0的正式发布是.NET ...

  9. 业余草分享 Spring Boot 2.0 正式发布的新特性

    就在昨天Spring Boot2.0.0.RELEASE正式发布,今天早上在发布Spring Boot2.0的时候还出现一个小插曲,将Spring Boot2.0同步到Maven仓库的时候出现了错误, ...

  10. .NET Standard 2.0正式发布了

    亦可赛艇 前天(2017年8月14日),.NET Standard 2.0正式版终于发布了,与之相配套的.NET Core 2.0也同时正式发布,真是令人振奋. 详情请看:https://blogs. ...

随机推荐

  1. Magicodes.IE 2.4版本发布

    今天我们发布了2.4版本,这离不开大家对Magicodes.IE的支持,我们也对大家的意见以及需求不断的进行更新迭代,目前我们的发布频率平均在一周一个beta版本,一个月一个正式版本的更新,我们欢迎更 ...

  2. 洛谷UVA524 素数环 Prime Ring Problem

    标签:搜索与回溯 题目: 从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数. 算法分析: 非常明显,这是一道回溯的题目.从1开始,每个空位有20种可能,只要填进去的数合法:与前面的数不相 ...

  3. Docker笔记4:在 CentOS 上安装 Docker

    Docker 是一个开源的应用容器引擎,主要有两个分支,一个是社区免费版(Docker CE),一个是企业版(Docker EE). 第1步:系统环境要求 Docker 支持的 CentOS 版本: ...

  4. javascript 原型与原型链浅析

    原型 和原型链 什么是原型链 简单理解就是原型组成的链,对象的__proto__它的是原型,而原型也是一个对象,也有__proto__属性,原型的__proto__又是原型的原型,就这样可以一直通过_ ...

  5. day39 Pyhton 并发编程02 后

    一.开启子进程的另一种方式 import os from multiprocessing import Process class MyProcess(Process): def __init__(s ...

  6. cmake引入三方库

    目录结构 . |-- cmake | |-- CompilerSettings.cmake | |-- Options.cmake | `-- ProjectJsonCpp.cmake |-- CMa ...

  7. linux(centos8):prometheus使用alertmanager发送报警邮件(prometheus 2.18.1/alertmanager 0.20.0)

    一,alertmanager的用途 1,Alertmanager的作用: Alertmanager是一个独立的报警模块, 它接收Prometheus等客户端发来的警报,并通过分组.删除重复等处理, 通 ...

  8. 列表的嵌套,元组和range()方法

    列表嵌套: 列表内嵌套列表 li = ['a','b',[1,2,3,["李白",'苏轼'],4,5],'c'] #取出"李白" print(li[2][3][ ...

  9. Spring笔记(4) - Spring的编程式事务和声明式事务详解

    一.背景 事务管理对于企业应用而言至关重要.它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性.就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到操作 ...

  10. C# POst 接收或发送XML

    摘自:http://www.cnblogs.com/Fooo/p/3529371.html 项目分成两个 web(ASP.Net)用户处理请求,客户端(wpf/winform)发送请求 1.web项目 ...