9 things about C# 9

Init 

Introducing an init which is variant of set can only be called during object initialization:

public class Employee
{
    public string Name { get; init; }
    public string Designation { get; init; }
}

Because init accessors can only be called during initialization, they are allowed to mutate readonly fields of the enclosing class

public class Employee
{
    private readonly string name;
    private readonly string designation;
    
    public string Name 
    { 
        get => name; 
        init => name = (value ??
                 throw new ArgumentNullException(nameof(Name)));
    }
    public string Designation
    { 
        get => designation; 
        init => designation = (value ??
                 throw new ArgumentNullException(nameof(Designation)));
    }
}


Data (Records)

The data keyword on the class declaration marks it as a record. Records are meant to be seen more as “values” – data! – and less as objects. They aren’t meant to have mutable encapsulated state. Instead you represent change over time by creating new records representing the new state. They are defined not by their identity, but by their contents.

public data class Employee
{
    public string Name { get; init; }
    public string Designation { get; init; }
}


With

When working with immutable data, a common pattern is to create new values from existing ones to represent a new state (non-destructive mutation).

To help with this style of programming, records allow for a new kind of expression; the with-expression:

var Consultant = employee with { Designation = "Manager" };

With-expressions use object initializer syntax to state what’s different in the new object from the old object. You can specify multiple properties.

A record implicitly defines a protected “copy constructor” – a constructor that takes an existing record object and copies it field by field to the new one:

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

The with expression causes the copy constructor to get called, and then applies the object initializer on top to change the properties accordingly.

If you don’t like the default behavior of the generated copy constructor you can define your own instead, and that will be picked up by the with expression.


Equal

All objects inherit a virtual Equals(object) method from the object class. This is used as the basis for the Object.Equals(object, object) static method when both parameters are non-null.

Structs override this to have “value-based equality”, comparing each field of the struct by calling Equals on them recursively. Records do the same (by using data keyword or for property by init keyword).

Similarly to the with-expression support, value-based equality also has to be “virtual”, in the sense that Employees need to compare all the Employee fields, even if the statically known type at the point of comparison is a base type like Employee. That is easily achieved by overriding the already virtual Equals method.

However, there is an additional challenge with equality: What if you compare two different kinds of Employee. We can’t really just let one of them decide which equality to apply: Equality is supposed to be symmetric, so the result should be the same regardless of which of the two objects come first. In other words, they have to agree on the equality being applied!

C# takes care of this for you automatically. The way it’s done is that records have a virtual protected property called EqualityContract. Every derived record overrides it, and in order to compare equal, the two objects musts have the same EqualityContract.

new - Target typed

“Target typing” is a term we use for when an expression gets its type from the context of where it’s being used. For instance null and lambda expressions are always target typed.

In C# 9.0 some expressions that weren’t previously target typed become able to be guided by their context.

new expressions in C# have always required a type to be specified. Now you can leave out the type if there’s a clear type that the expressions is being assigned to.

Line ln = new (2, 23);


??, ? - Target typed

Sometimes conditional ?? and ?: expressions don’t have an obvious shared type between the branches. Such cases fail today, but C# 9.0 will allow them if there’s a target type that both branches convert to:

Employee emp = manager ?? consultant; // Shared base type
int? counter = cnt ? 0 : null; // nullable value type

Top level programs 

In C# 9.0 we can choose to write our main program at the top level instead:

using System;

Console.WriteLine("Hello World!");

Any statement is allowed. The program has to occur after the using and before any type or namespace declarations in the file, and you can only do this in one file, just as you can have only one Main method today.

If you want to return a status code you can do that. If you want to await things you can do that. And if you want to access command line arguments, args is available as a “magic” parameter.


Pattern Matching improved

Simple type patterns

Currently, a type pattern needs to declare an identifier when the type matches – even if that identifier is a discard _, as in DeliveryTruck _ above. But now you can just write the type:

DeliveryTruck => 10.00m,

Relational patterns

C# 9.0 introduces patterns corresponding to the relational operators <<= and so on. So you can now write the DeliveryTruck part of the above pattern 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.

Logical patterns

Finally you can combine patterns with logical operators andor and not, spelled out as words to avoid confusion with the operators used in expressions. For instance, the cases of the nested switch above could be put into ascending order like this:

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

The middle case there uses and to combine two relational patterns and form a pattern representing an interval.


Covariant Returns

It’s sometimes useful to express that a method override in a derived class has a more specific return type than the declaration in the base type. C# 9.0 allows that:

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


The best place to check out the full set of upcoming features for C# 9.0 on the Roslyn (C#/VB Compiler) GitHub repo.


Comments

Popular posts from this blog

Using C#9 record and init property in your .NET Framework 4.x, .NET Standard and .NET Core projects

Where you can locate files in Telligent Community DB Server Site?

Easy Steps to migrate from Telligent CS to SP Online