C # (016): new features of C# 9.0 (NET Framework 5.0 and Visual Studio 2019 v16.8)

Original text: 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 patterns corresponding to relational operators <, < = and so on. 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,
    },
    

Here > 5000 and < 3000 are relational patterns.

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!

Reference link: https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-9

Posted by djs1 on Sun, 15 May 2022 05:09:04 +0300