C#(016):C# 9.0 新特性( NET Framework 5.0 与 Visual Studio 2019 v16.8)(C # (016): new features of C # 9.0 (net framework 5.0 and visual studio 2019 v16.8))

原文:https://blog.csdn.net/csdnnews/article/details/106345959

微软正在推进C# 9.0的开发,C# 9.0 将成为.NET 5 开发平台的一部分,预计于 11 月发布。微软.NET团队C#首席设计师Mads
Torgersen表示,C# 9.0已初具规模,本文就分享下该语言下一版本中添加的一些主要功能。

C#的每个新版本都力求提升通用编程方面的清晰度与简单性,C# 9.0也不例外,尤其注重支持数据形状的简洁与不可变表示。下面,我们就来详细介绍!

一、仅可初始化的属性

对象的初始化器非常了不起。它们为客户端创建对象提供了一种非常灵活且易于阅读的格式,而且特别适合嵌套对象的创建,我们可以通过嵌套对象一次性创建整个对象树。下面是一个简单的例子:

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

对象初始化器还可以让程序员免于编写大量类型的构造样板代码,他们只需编写一些属性即可!

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

目前的一大限制是,属性必须是可变的,只有这样对象初始化器才能起作用,因为它们需要首先调用对象的构造函数(在这种情况下调用的是默认的无参构造函数),然后分配给属性设置器。

仅可初始化的属性可以解决这个问题!它们引入了init访问器。init访问器是set访问器的变体,它只能在对象初始化期间调用:

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

在这种声明下,上述客户端代码仍然合法,但是后续如果你想为FirstName和LastName属性赋值就会出错。

01、初始化访问器和只读字段

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

二、记录

如果你想保持某个属性不变,那么仅可初始化的属性非常有用。如果你希望整个对象都不可变,而且希望其行为宛如一个值,那么就应该考虑将其声明为记录:

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

上述类声明中的data关键字表明这是一个记录,因此它具备了其他一些类似于值的行为,后面我们将深入讨论。一般而言,我们更应该将记录视为“值”(数据),而非对象。它们不具备可变的封装状态。相反,你可以通过创建表示新状态的新记录来表示随着时间发生的变化。记录不是由标识确定,而是由其内容确定。

01、With表达式

处理不可变数据时,一种常见的模式是利用现有的值创建新值以表示新状态。例如,如果想修改某人的姓氏,那么我们会用一个新对象来表示,这个对象除了姓氏之外和旧对象完全一样。通常我们称该技术为非破坏性修改。记录代表的不是某段时间的某个人,而是给定时间点上这个人的状态。

为了帮助大家习惯这种编程风格,记录允许使用一种新的表达方式:with表达式:

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

with表达式使用对象初始化的语法来说明新对象与旧对象之间的区别。你可以指定多个属性。

记录隐式地定义了一个protected “复制构造函数”,这种构造函数利用现有的记录对象,将字段逐个复制到新的记录对象中:

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

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

如果你不喜欢自动生成的复制构造函数,那么也可以自己定义,with表达式就会调用自定义的复制构造函数。

02、基于值的相等性

所有对象都会从object类继承一个虚的Equals(object)方法。在调用静态方法Object.Equals(object,
object)且两个参数均不为null时,该Equals(object)就会被调用。

结构体可以重载这个方法,获得“基于值的相等性”,即递归调用Equals来比较结构的每个字段。记录也一样。

这意味着,如果两个记录对象的值一致,则二者相等,但两者不一定是同一对象。例如,如果我们再次修改前面那个人的姓氏:

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

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

如果你不喜欢自动生成的Equals覆盖默认的逐字段比较的行为,则可以编写自己的Equals重载。你只需要确保你理解基于值的相等性在记录中的工作原理,尤其是在涉及继承的情况下,具体的内容我们稍后再做介绍。

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

03、数据成员

在绝大多数情况下,记录都是不可变的,它们的仅可初始化的属性是公开的,可以通过with表达式进行非破坏性修改。为了优化这种最常见的情况,我们改变了记录中类似于string
FirstName这种成员声明的默认含义。

在其他类和结构声明中,这种声明表示私有字段,但在记录中,这相当于公开的、仅可初始化的自动属性!因此,如下声明:

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

与之前提到过的下述声明完全相同:

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

我们认为这种方式可以让记录更加优美而清晰。如果你需要私有字段,则可以明确添加private修饰符:

    private string firstName;
    

04、位置记录

有时,用参数位置来声明记录会很有用,内容可以根据构造函数参数的位置来指定,并且可以通过位置解构来提取。

你完全可以在记录中指定自己的构造函数和析构函数:

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

但是,我们可以用更短的语法表达完全相同的内容(使用成员变量的大小写方式来命名参数):

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

上述声明了仅可初始化的公开的自动属性以及构造函数和析构函数,因此你可以这样写:

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

如果你不喜欢生成的自动属性,则可以定义自己的同名属性,这样生成的构造函数和析构函数就会自动使用自己定义的属性。

05、记录和修改

记录的语义是基于值的,因此在可变的状态中无法很好地使用。想象一下,如果我们将记录对象放入字典,那么就只能通过Equals和GethashCode找到了。但是,如果记录更改了状态,那么在判断相等时它代表的值也会发生改变!可能我们就找不到它了!在哈希表的实现中,这个性质甚至可能破坏数据结构,因为数据的存放位置是根据它“到达”哈希表时的哈希值决定的!

而且,记录也可能有一些使用内部可变状态的高级方法,这些方法完全是合理的,例如缓存。但是可以考虑通过手工重载默认的行为来忽略这些状态。

06、with表达式与继承

众所周知,考虑继承时基于值的相等性和非破坏性修改是一个难题。下面我们在示例中添加一个继承的记录类Student:

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

在如下with表达式的示例中,我们实际创建一个Student,然后将其存储到Person变量中:

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

在最后一行的with表达式中,编译器并不知道person实际上包含一个Student。而且,即使otherPerson不是Student对象,它也不是合法的副本,因为它包含了与第一个对象相同的ID属性。

C#解决了这个问题。记录有一个隐藏的虚方法,能够确保“克隆”整个对象。每个继承的记录类型都会通过重载这个方法来调用该类型的复制构造函数,而继承记录的复制构造函数会调用基类的复制构造函数。with表达式只需调用这个隐藏“clone”方法,然后在结果上应用对象初始化器即可。

07、基于值的相等性与继承

与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可能会有不同的看法!我们需要确保二者都认同它们是不同的对象。

C#可以自动为你解决这个问题。具体的实现方式是:记录拥有一个名为EqualityContract的受保护虚属性。每个继承的记录都会重载这个属性,而且为了比较相等,两个对象必须具有相同的EqualityContract。

三、顶层程序

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

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

这不仅对初学者来说难度太高,而且代码混乱,缩进级别也太多。

在C# 9.0中,你只需编写顶层的主程序:

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

任何语句都可以。程序必须位于using之后,文件中的任何类型或名称空间声明之前,而且只能在一个文件中,就像只有一个Main方法一样。

如果你想返回状态代码,则可以利用这种写法。如果你想await,那么也可以这么写。此外,如果你想访问命令行参数,则args可作为“魔术”参数使用。

局部函数是语句的一种形式,而且也可以在顶层程序中使用。在顶层语句之外的任何地方调用局部函数都会报错。

四、改进后的模式匹配

C# 9.0中添加了几种新的模式。下面我们通过如下模式匹配教程的代码片段来看看这些新模式:

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

01、简单类型模式

当前,类型模式需要在类型匹配时声明一个标识符,即使该标识符是表示放弃的_也可以,如上面的DeliveryTruck _。而如今你可以像下面这样编写类型:

    DeliveryTruck => 10.00m,
    

02、关系模式

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

03、逻辑模式

最后,你还可以将模式与逻辑运算符(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。例如,我们可以根据是否为null来拆分未知情况的处理方式:

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

此外,如果if条件中包含is表达式,那么使用not也很方便,可以避免笨拙的双括号:

    if (!(e is Customer)) { ... }
    

你可以这样写:

    if (e is not Customer) { ... }
    

五、改进后的目标类型推断

“目标类型推断”指的是表达式从所在的上下文中获取类型。例如,null和lambda表达式始终是目标类型推断。

在C# 9.0中,有些以前不是目标类型推断的表达式也可以通过上下文来判断类型。

01、支持目标类型推断的new表达式

C# 中的new表达式始终要求指定类型(隐式类型的数组表达式除外)。现在, 如果有明确的类型可以分配给表达式,则可以省去指定类型。

    Point p = new (3, 5);
    

02、目标类型的??与?:

有时,条件判断表达式中??与?:的各个分支之间并不是很明显的同一种类型。现在这种情况会出错,但在C#
9.0中,如果两个分支都可以转换为目标类型,就没有问题:

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

03、协变的返回值

有时,我们需要表示出继承类中重载的某个方法的返回类型要比基类中的类型更具体。C# 9.0允许以下写法:

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

六、更多内容

更多有关C#
9.0推出的新功能,请参照这个GitHub代码库(https://github.com/dotnet/roslyn/blob/master/docs/Language Feature Status.md)。

编程快乐!

参考链接:https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-9

————————

原文:https://blog.csdn.net/csdnnews/article/details/106345959

Microsoft is promoting the development of c# 9.0, and c# 9.0 will become Net 5 development platform, which is expected to be released in November. Microsoft Net team c# chief designer MADS
Torgersen said that c# 9.0 has begun to take shape. This article will share some main functions added in the next version of the language.

Each new version of C # strives to improve the clarity and simplicity of general programming, and C # 9.0 is no exception, especially focusing on supporting the concise and immutable representation of data shapes. Next, let’s introduce it in detail!

1、 Only initializable properties

Object initializers are amazing. They provide a very flexible and easy to read format for the client to create objects, and are especially suitable for the creation of nested objects. We can create the whole object tree at one time through nested objects. Here is a simple example:

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

Object initializers can also save programmers from writing a large number of types of construction template code. They only need to write some properties!

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

One of the current limitations is that properties must be mutable so that object initializers can work, because they need to call the object’s constructor first (in this case, the default parameterless constructor) and then assign it to the property setter.

Only initializable properties can solve this problem! They introduce init accessors. Init accessor is a variant of set accessor. It can only be called during object initialization:

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

Under this declaration, the above client code is still legal, but later if you want to assign values to the firstname and LastName attributes, you will make an error.

01. Initialize accessors and read-only fields

Since init accessors can only be called during initialization, they can modify the read-only field of their class, just like constructors.

    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、 Record

Only initializable properties are useful if you want to keep a property unchanged. If you want the entire object to be immutable and behave like a value, you should consider declaring it as a record:

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

The data keyword in the above class declaration indicates that this is a record, so it has some other value like behaviors, which will be discussed in depth later. Generally speaking, we should treat records more as “values” (data) than objects. They do not have a variable packaging state. Instead, you can represent changes over time by creating new records that represent new states. Records are not determined by identification, but by their content.

01. With expression

When dealing with immutable data, a common pattern is to create a new value from an existing value to represent a new state. For example, if we want to change someone’s last name, we will use a new object, which is exactly the same as the old object except for the last name. We usually call this technology non-destructive modification. The record does not represent a person in a certain period of time, but the state of the person at a given point in time.

In order to help people get used to this programming style, the record allows a new expression: with expression:

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

The with expression uses the syntax of object initialization to illustrate the difference between a new object and an old object. You can specify multiple properties.

The record implicitly defines a protected “copy constructor”, which uses the existing record object to copy the fields one by one into the new record object:

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

The with expression calls the copy constructor and then applies an object initializer on it to change the properties accordingly.

If you don’t like the automatically generated copy constructor, you can also define it yourself, and the with expression will call the custom copy constructor.

02. Value based equality

All objects inherit a virtual equals (object) method from the object class. When calling the static method object Equals(object,
Object) and the two parameters are not null, the equals (object) will be called.

The structure can overload this method to obtain “value based equality”, that is, recursively call equals to compare each field of the structure. So is the record.

This means that if the values of two record objects are the same, they are equal, but they are not necessarily the same object. For example, if we change the last name of the previous person again:

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

Now, referenceequals (person, original person) = false (they are not the same object), but equals (person,
Originalperson) = true (they have the same value).

If you don’t like automatically generated equals overriding the default field by field comparison behavior, you can write your own equals overload. You just need to make sure you understand how value based equality works in records, especially when inheritance is involved. We’ll talk about it later.

In addition to value based equals, there is a value based gethashcode() overloaded method.

03. Data member

In most cases, records are immutable. Their initializable properties are public and can be modified non destructively through the with expression. In order to optimize this most common case, we changed the record similar to string
Firstname is the default meaning of this member declaration.

In other class and structure declarations, this Declaration represents a private field, but in records, this is equivalent to an open, initializable only automatic attribute! Therefore, the following statement:

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

Exactly the same as the following statement mentioned earlier:

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

We think this way can make the record more beautiful and clear. If you need private fields, you can explicitly add the private modifier:

    private string firstName;
    

04. Location record

Sometimes, it is useful to declare records with parameter positions. The content can be specified according to the position of constructor parameters and can be extracted by position deconstruction.

You can specify your own constructor and destructor in the record:

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

However, we can express exactly the same content in a shorter syntax (using the case of member variables to name parameters):

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

The above declaration only initializable public automatic attributes and constructors and destructors, so you can write as follows:

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

If you don’t like the generated automatic attributes, you can define your own attributes with the same name, so that the generated constructors and destructors will automatically use the attributes you define.

05. Record and modification

The semantics of records are value based, so they cannot be used well in variable states. Imagine that if we put the record object into the dictionary, we can only find it through equals and GetHashCode. However, if the record changes its state, the value it represents will also change when judging equality! Maybe we can’t find it! In the implementation of hash table, this property may even destroy the data structure, because the storage location of data is determined by the hash value when it “reaches” the hash table!

Moreover, records may also have some advanced methods using internal variable state, which are completely reasonable, such as caching. However, you can consider ignoring these states by manually overloading the default behavior.

06. With expression and inheritance

As we all know, value based equality and non-destructive modification is a difficult problem when considering inheritance. Next, we add an inherited record class student in the example:

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

In the following example of the with expression, we actually create a student and store it in the person variable:

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

In the with expression on the last line, the compiler does not know that person actually contains a student. Moreover, even if otherperson is not a student object, it is not a legal copy because it contains the same ID attribute as the first object.

C # solved this problem. The record has a hidden virtual method that ensures that the entire object is “cloned”. Each inherited record type will call the copy constructor of the type by overloading this method, and the copy constructor of the inherited record will call the copy constructor of the base class. The with expression simply calls the hidden “clone” method and applies an object initializer to the result.

07. Value based equality and inheritance

Similar to the support of with expression, value based equality must also be “virtual”, that is, when comparing two student objects, all fields need to be compared. Even when comparing, you can statically know that the type is the base class, such as person. This can be easily achieved by overriding the equals method, which is already a virtual method.

However, there is another problem with Equality: what if you need to compare two different types of people? We cannot simply choose one of them to decide whether they are equal or not: equality should be symmetrical, so no matter which of the two objects appears first, the result should be the same. In other words, equality must be agreed between the two!

Let’s give an example to illustrate this problem:

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

Are the two objects equal to each other? Person1 may be considered equal because person2 has all the fields of person, but person2 may have different views! We need to make sure that both agree that they are different objects.

C # can automatically solve this problem for you. The specific implementation method is: the record has a protected virtual attribute called equalitycontract. Each inherited record overloads this property, and in order to compare equality, the two objects must have the same equalitycontract.

3、 Top level program

Using C # to write a simple program requires a lot of template code:

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

This is not only too difficult for beginners, but also the code is chaotic and there are too many indentation levels.

In c# 9.0, you only need to write the top-level main program:

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

Any statement is OK. The program must be located after using, before any type or namespace declaration in the file, and only in one file, just as there is only one main method.

If you want to return the status code, you can use this method. You can write await if you want. In addition, if you want to access command line parameters, args can be used as a “magic” parameter.

Local functions are a form of statements and can also be used in top-level programs. Calling a local function anywhere other than the top-level statement will report an error.

4、 Improved pattern matching

Several new patterns have been added to c# 9.0. Let’s take a look at these new patterns through the code snippet of the following pattern matching tutorial:

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

01. Simple type mode

Currently, the type pattern needs to declare an identifier when the type matches, even if the identifier indicates abandonment_ You can also, such as deliverytrack. Now you can write types like this:

    DeliveryTruck => 10.00m,
    

02. Relationship model

C # 9.0 introduces and relational operators & lt& lt;= Equivalent mode. Therefore, you can write the deliverytrack of the above mode as a nested switch expression:

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

& gt; here; 5000 and & lt; 3000 is a relational model.

03. Logic mode

Finally, you can combine patterns with logical operators (and, or and not), which appear in English words to avoid confusion with the operators used in expressions. For example, the above nested switch expression can be written in ascending order as follows:

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

The middle row combines the two relational patterns through and to form a pattern representing the interval.

The common usage of the not pattern can also be applied to the null constant pattern, such as not null. For example, we can split the processing method of unknown situations according to whether it is null:

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

In addition, if the if condition contains an is expression, it is also convenient to use not to avoid clumsy double parentheses:

    if (!(e is Customer)) { ... }
    

You can write this:

    if (e is not Customer) { ... }
    

5、 Improved target type inference

“Target type inference” means that the expression gets the type from the context in which it is located. For example, null and lambda expressions are always target type inference.

In c# 9.0, some expressions that were not inferred from the target type can also be judged by context.

01. New expression supporting target type inference

New expressions in C # always require the specified type (except for implicitly typed array expressions). Now, if there is an explicit type that can be assigned to an expression, you can omit specifying the type.

    Point p = new (3, 5);
    

02. Target type?? And?:

Sometimes, in the conditional judgment expression?? And?: The branches of are not obviously of the same type. Now this situation will go wrong, but in C#
In 9.0, if both branches can be converted to the target type, there is no problem:

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

03. Covariant return value

Sometimes, we need to show that the return type of an overloaded method in the inheritance class is more specific than the type in the base class. C# 9.0 allows the following:

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

6、 More content

More about C#
For the new functions launched in 9.0, please refer to this GitHub code base( https://github.com/dotnet/roslyn/blob/master/docs/Language Feature Status. md)。

Happy programming!

参考链接:https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-9