9 things about C# 9
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 and
, or
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
Post a Comment