C#9.0主要特性的一些想法

前言

翻译自 Mads Torgersen 2020年5月20日的博文《Welcome to C# 9.0》,Mads Torgersen 是微软 C# 语言的首席设计师,也是微软 .NET 团队的项目群经理。

C# 9.0 正在成形,我想和大家分享一下我们对下一版本语言中添加的一些主要特性的想法。

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

就让我们一探究竟吧!

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

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

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

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

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 属性的任何赋值都是错误的。

初始化(init) 访问器和只读(readonly)字段

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

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

二、记录(record)

译者注:

原文中声明一个记录的 data class ** 联合关键字现在已经变成 record 关键字了,所以翻译过程中做了修正。

如果您想使单个属性不可变,那么仅初始化(init-only)属性是极好的。如果您想要整个对象是不可变的,行为像一个值,那么你应该考虑声明它为一个记录(record):

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

对于记录(record),赋予了它一些类似值的行为,我们将在下面深入探讨。一般来说,记录更应该被看作是“值”——数据(data),而不是对象!它们并不具有可变的封装状态,相反,您需要通过创建表示新状态的新记录来表示其随时间的变化。它们不是由它们的身份(identity)确定的,而是由它们的内容确定的。

with 表达式

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

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

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

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

记录(record)隐式定义了一个受保护的(protected)“复制构造函数”——一个接受现有记录对象并逐字段将其复制到新记录对象的构造函数:

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

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

如果您不喜欢生成的“复制构造函数”的默认行为,您可以定义自己的“复制构造函数”,它将被 with 表达式捕获。

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

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

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

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

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

现在我们将得到 ReferenceEquals(person, originalPerson) = false(它们不是同一个对象),但是 Equals(person, originalPerson) = true(它们有相同的值)。

如果您不喜欢生成的 Equals 重写的默认逐个字段比较的行为,您可以自己编写。您只需要注意理解“基于值的相等”是如何在记录(record)中工作的,特别是在涉及继承时,我们后面会讲到。

除了基于值的 Equals 之外,还有一个基于值 GetHashCode() 的重写。

数据成员(Data members)

绝大多数情况下,记录(record)都是不可变的,仅初始化(init-only)公共属性可以通过 with 表达式进行非破坏性修改。为了对这种常见情况进行优化,记录(record)更改了 string FirstName 这种形式的简单成员声明的默认含义,与其他类和结构体声明中的隐式私有字段不同,它被当作是一个公共的、仅初始化(init-only) 自动属性的简写!因此,声明:

public record Person { string FirstName; string LastName; }

与我们之前的声明意思完全一样,即等同于声明:

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

我们认为这有助于形成漂亮而清晰的记录(record)声明。如果您确实需要私有字段,只需显式添加 private 修饰符:

private string firstName;

位置记录(Positional records)

有时,对记录(record)采用位置更明确的方法是有用的,其中它的内容是通过构造函数参数提供的,并且可以通过位置解构来提取。

完全可以在记录(record)中指定自己的构造函数和解构函数:

public record Person
{
 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);
}

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

public record Person(string FirstName, string LastName);

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

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

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

记录与可变性(Records and mutation)

记录(record)的基于值的语义不能很好地适应可变状态。想象一下,将一个记录(record)对象放入字典中。再次查找它依赖于 Equals 和 GetHashCode(有时)。但是如果记录改变了状态,它的 Equals 值也会随之改变,我们可能再也找不到它了!在哈希表实现中,它甚至可能破坏数据结构,因为位置是基于它的哈希码得到的。

记录(record)内部的可变状态或许有一些有效的高级用法,特别是对于缓存。但是重写默认行为以忽略这种状态所涉及的手工工作很可能是相当大的。

with 表达式和继承(With-expressions and inheritance)

众所周知,基于值的相等和非破坏性突变与继承结合在一起时是极具挑战性的。让我们在运行示例中添加一个派生的记录(record)类 Student:

public record Person { string FirstName; string LastName; }
public record Student : Person { int ID; }

然后,让我们从 with 表达式示例开始,实际地创建一个 Student,但将它存储在 Person 变量中:

int newId = 1;
Func<int> GetNewId = () => ++newId;
//上面两上是译者在测试时发现需要添加的代码。

Person person = new Student { FirstName = "Scott", LastName = "Hunter", ID = GetNewId() };
otherPerson = person with { LastName = "Hanselman" };

在最后一行带 with 表达式的地方,编译器不知道 person 实际上包含 Student。然而,如果新的 person(即 otherPerson) 不是一个真正的 Student 对象,并且具有从第一个 person 复制过去的相同的 ID,那么它就不是一个恰当的拷贝。

C# 实现了这一点。记录(record)有一个隐藏的虚方法(virtual method),它被委托“克隆”整个对象。每个派生记录类型都重写此方法以调用该类型的复制构造函数,并且派生记录的复制构造函数将链接到基记录的复制构造函数。with 表达式只需调用隐藏的“克隆”方法并将对象初始化器应用于其返回结果。

基于值的相等和继承(Value-based equality and inheritance)

与 with 表达式支持类似,基于值的相等也必须是“虚的(virtual)”,即 Student 需要比较 Student 的所有字段,即使比较时静态已知的类型是 Person 之类的基类型。这很容易通过重写虚的(virtual) Equals 方法来实现。

然而,关于相等还有一个额外的挑战:如果你比较两种不同的 Person 会怎样?我们不能仅仅让其中一个来决定实施哪个相等:相等应该是对称的,所以不管两个对象哪个在前面,结果应该是相同的。换句话说,它们必须在相等的实施上达成一致!

举例说明一下这个问题:

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

这两个对象相等吗? person1 可能会认为相等,因为 person2 对于 Person 的所有属性都是正确的,但是 person2 不敢苟同!我们需要确保它们都同意它们是不同的对象。

同样,C# 会自动为您处理这个问题。实现的方式是,记录有一个名为 EqualityContract 的“虚的(virtual)”受保护的属性。每个派生记录(record)都会重写它,为了比较相等,这两个对象必须具有相同的 EqualityContract。

三、顶级程序(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 可以作为一个“魔法”参数使用。

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

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

C# 9.0 中添加了几种新的模式。让我们从模式匹配教程(https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/pattern-matching)的代码片段的上下文中来看看它们:

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)

最后,您可以将模式与逻辑运算符 and、or 和 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) { ... }

五、改进的目标类型(Improved target typing)

“目标类型(Target typing)”是一个术语,当一个表达式从使用它的地方的上下文中获得其类型时,我们使用这个术语。例如,null 和 lambda表达式始终是目标类型的。

在 C# 9.0 中,一些以前不是目标类型的表达式变得可以由其上下文推导。

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

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

Point p = new (3, 5);

目标类型的 ?? 和 ?:(Target typed ?? and ?:)

有时有条件的 ?? 和 ?: 表达式在分支之间没有明显的共享类型,这种情况目前是失败的。但是如果有一个两个分支都可以转换成的目标类型,在 C# 9.0 中将是允许的。

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

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

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

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

更多内容……

要查看 C# 9.0 即将发布的全部特性并追随它们的完成,最好的地方是 Roslyn(C#/VB 编译器) GitHub 仓库上的 Language Feature Status(https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md)。

总结

到此这篇关于C#9.0主要特性的一些想法的文章就介绍到这了,更多相关C#9.0主要特性内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#9.0 新特性简介

    CandidateFeaturesForCSharp9 看到标题,是不是认为我把标题写错了?是的,C# 8.0还未正式发布,在官网它的最新版本还是Preview 5,通往C#9的漫长道路却已经开始.前写天收到了活跃在C#一线的BASSAM ALUGILI给我分享C# 9.0新特性,我在他文章的基础上进行翻译,希望能对大家有所帮助. 这是世界上第一篇关于C#9候选功能的文章.阅读完本文后,你将会为未来可能遇到的C# 9.0新特性做好更充分的准备. 这篇文章基于, C# 9.0候选新特性 原生大小的

  • C# 9.0 新特性之模式匹配简化的实现

    记得在 MS Build 2020 大会上,C# 语言开发项目经理 Mads Torgersen 宣称 C# 9.0 将会随着 .NET 5 在今年 11 月份正式发布.目前 .NET 5 已经到了 Preview 5 阶段了,C# 9.0 也已经初具规模.忍不住激动的心情,暂停更新<C#.NET 拾遗补漏>系列几天,先要和大家分享一下我了解到的 C# 9.0 的新特性.由于新特性比较多,所以会分成几篇来讲.这是第一篇,专讲模式匹配这个特性的简化. 模式匹配(Pattern Matching)

  • 浅谈C#9.0新特性之参数非空检查简化

    参数非空检查是缩写类库很常见的操作,在一个方法中要求参数不能为空,否则抛出相应的异常.比如: public static string HashPassword(string password) { if(password is null) { throw new ArgumentNullException(nameof(password)); } ... } 当异常发生时,调用者很容易知道是什么问题.如果不加这个检查,可能就会由系统抛出未将对象引用为实例之类的错误,这不利于调用者诊断错误. 由

  • 浅谈C# 9.0 新特性之只读属性和记录

    大家好,这是 C# 9.0 新特性系列的第 4 篇文章. 熟悉函数式编程的童鞋一定对"只读"这个词不陌生.为了保证代码块自身的"纯洁",函数式编程是不能随便"弄脏"外来事物(参数.变量等)的,所以"只读"对函数式编程非常重要. 为了丰富 C# 对函数式编程支持,较新的 C# 版本引入了一些很有用的新特性.比如 C# 8 中就对 struct 类型的方法增加了 readonly 修饰符支持,被 readonly 修饰的方法是不能

  • 浅析C# 9.0 新特性之 Lambda 弃元参数

    大家好,这是 C# 9.0 新特性短系列的第 5 篇文章. 弃元(Discards) 是在 C# 7.0 的时候开始支持的,它是一种人为丢弃不使用的临时虚拟变量.语法上它是用来赋值的,但它却不被分配存储空间,即没有值,所以不能从中读取值.弃元用 _(下划线) 表示,下划线是一个关键字,只能赋值,不能读取,例如: 在 C# 7.0 中,弃元的使用场景主要有下面四种: 元组和对象的解构 使用 is 和 switch 的模式匹配 对具有 out 参数的方法的调用 作用域内独立使用场景 针对这几个场景,

  • c# 9.0新特性nint和Pattern matching的使用方法

    一:背景 1. 讲故事 上一篇跟大家聊到了Target-typed new 和 Lambda discard parameters,看博客园和公号里的阅读量都达到了新高,甚是欣慰,不管大家对新特性是多头还是空头,起码还是对它抱有一种极为关注的态度,所以我的这个系列还得跟,那就继续开撸吧,今天继续带来两个新特性,更多新特性列表,请大家关注:新特性预览 二:新特性研究 1. Native ints 从字面上看貌似是什么原生类型ints,有点莫名其妙,还是看一看Issues上举得例子吧: Summar

  • C#9.0主要特性的一些想法

    前言 翻译自 Mads Torgersen 2020年5月20日的博文<Welcome to C# 9.0>,Mads Torgersen 是微软 C# 语言的首席设计师,也是微软 .NET 团队的项目群经理. C# 9.0 正在成形,我想和大家分享一下我们对下一版本语言中添加的一些主要特性的想法. 对于 C# 的每一个新版本,我们都在努力让常见编码场景的实现变得更加清晰和简单,C# 9.0 也不例外.这次特别关注的是支持数据模型的简洁和不可变表示. 就让我们一探究竟吧! 一.仅初始化(ini

  • conda安装tensorflow和conda常用命令小结

    1. 在conda的一个环境下安装tensorflow 1)先查看它的各个版本: conda search tensorflow 或者 conda search tensorflow-gpu 2)选择一个版本安装: conda install tensorflow=0.10.0rc0 其他命令 更新: conda update xxx 删除包: conda remove xxx 2. conda的一些命令 添加环境: (e.g. 名称为py36,python版本为3.6) conda creat

  • C语言猜凶手及类似题目的实现示例

    目录 描述: 思路及分析: 现在,我们来看和它相似的第二道题目: 描述: 思路及分析: 第三道题目: 描述: 思路及分析: 总结: 描述: 日本某地发生了一件谋杀案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个. 以下为4个嫌疑犯的供词: A说:不是我. B说:是C. C说:是D. D说:C在胡说 已知3个人说了真话,1个人说的是假话. 现在请根据这些信息,写一个程序来确定到底谁是凶手. 思路及分析: 从来没有做过类似题目的同学第一次看见这道题,可能有点发懵.然后开始考虑假设,排列组合,枚举等等

  • java中的数组初始化赋初值方式

    目录 java数组初始化赋初值 方法一 方法二 方法三 数组互相赋值方式 使用for循环 使用Object的clone() 使用System的静态方法arraycopy() java数组初始化赋初值 方法一 int[] vis1;//声明未初始化      vis1=new int[100];//定义占用空间大小(100个int)     for(int i=1;i<=5;i++)     {         vis1[i]=i;//进行赋值     }     for(int i=1;i<1

  • Web2.0编程思想:16条法则

    1.在你开始之前,先定一个简单的目标.无论你是一个Web 2.0应用的创建者还是用户,请清晰的构思你的目标.就像"我需要保存一个书签"或者"我准备帮助人们创建可编辑的.共享的页面"这样的目标,让你保持最基础的需求.很多Web 2.0应用的最初吸引之处就是它的简单,避免并隐藏了那些多余的复杂性.站在创建者的立场,可以想象Google的几乎没有内容的主页,还有del.icio.us的简单的线条.从最终用户的角度来看,与之齐名的就是Diggdot.us所提供的初始化页面.

  • Vue 2.0学习笔记之Vue中的computed属性

    Vue中的 computed 属性称为 计算属性 .在这一节中,我们学习Vue中的计算属性如何使用?记得在学习Vue的模板相关的知识的时候,知道在模板内可以使用表达式,而且模板内的表达式是非常的便利,但这种遍历是有一定的限制的,它们实际上是用于一些简单的运算.也就是说,如果在模板中放入太多的逻辑会让模板过重而且难以维护.咱们先来看一个示例: <div id="app"> <h1>{{ message.split('').reverse().join('') }}

  • C# 8.0新特性介绍

    C# 语言是在2000发布的,至今已正式发布了7个版本,每个版本都包含了许多令人兴奋的新特性和功能更新.同时,C# 每个版本的发布都与同时期的 Visual Studio 以及 .NET 运行时版本高度耦合,这也有助于开发者更好的学习掌握 C#,并将其与 Visual Studio 以及 .NET 的使用结合起来. 加快 C# 版本的发布速度 在被称为"新微软"的推动下,微软创新的步伐也加快了.为了做到加快步伐,微软开发部门将一些过去集成在一起的技术现在都分离了出来. Visual S

  • JavaBean(EJB) 3.0 全新体验

    引言 期待以久的EJB3.0规范在最近发布了它的初稿.在本文中将对新的规范进行一个概要性的介绍,包括新增的元数据支持,EJBQL的修改,实体Bean模型访问bean上下文的新方法和运行时环境等等.作者还讨论了EJB在未来要作出的调整以及EJB3.0与其他开发规范之间的关系. 开始 无论如何由于EJB的复杂性使之在J2EE架构中的表现一直不是很好.EJB大概是J2EE架构中唯一一个没有兑现其能够简单开发并提高生产力的组建.EJB3.0规范正尝试在这方面作出努力以减轻其开发的复杂性.EJB3.0减轻

  • 在ASP.NET 2.0中操作数据之二十七:创建自定义排序用户界面

    简介 显示大量已经按类别(不是很多)排序的数据但没有类别分界线,用户很难找到所需要的类别.例如,数据库中只有9个类别(8个不同的类别和1个null),共81种产品.现在用一个GridView列出所有产品,假设有用户对类别Seafood的产品感兴趣,她一定会按类别排序,把Seafood产品排列在一起.排序后,用户便寻找Seafood产品开始和结束的地方.虽然是按英文字母排列类别不难找到Seafood,但仍要花些时间在GridView寻找.为了进一步的区分类别,许多网站使用类别分界线这种排序用户界面

  • JavaScript从0开始构思表情插件

    前言: 由于公司开发项目需要用到表情插件,在网上百度了好久,很多表情插件,都是需要引用好多js文件,也没有现成的demo可以使用,还有一些插件是引用好多图片,每一个表情都要重新请求一下.为了这样一个功能,要引入好多js,img,也是得不偿失-- 所以,博主自己码了一个小巧的"表情插件",方便以后项目直接使用. 功能 功能:传递表情对应的字符格式到后台,后台返回字符串,前端将该字符串解析展示成相应的表情展示在页面上. 使用方法: 在option中配置需要的参数 var option =

随机推荐