Loupe

Les nouveautés de C# 9

La semaine dernière a eu lieu la Build, événement annuel de Microsoft, qui pour la première fois se tenait entièrement en ligne et gratuitement. Les annonces ont été nombreuses : .NET 5, Blazor WebAssembly, .NET MAUI, WinUI 3... Mais ce qui a le plus attiré mon attention a été l'annonce de C# 9, qui introduit quelques nouveautés sympathiques. La liste est longue, je ne couvrirai donc pas tout, mais je vais tâcher de mettre en avant celles qui me semblent particulièrement intéressantes.

Note: Malheureusement les nouvelles features de C# 9 ne sont pas encore supportées dans la dernière preview du SDK, on ne peut donc pas encore jouer avec dans un vrai projet. Certaines features peuvent être testées sur SharpLab, mais les choses changent très vite, donc ce qui est disponible sur SharpLab ne reflète pas toujours ce qui a été annoncé à la Build.

Target-typed new

En C# 9, il sera possible d'omettre le type dans une expression de création d'objet, à condition que le type puisse être inféré d'après le contexte. Cela rend le code plus concis et moins répétitif. Par exemple :

private Dictionary<string, object> _properties = new();

Vérification de nullité des paramètres

Cette feature introduit une syntaxe très simple pour vérifier automatiquement si un paramètre d'une méthode est null. Par exemple, le code suivant:

public string SayHello(string name)
{
    if (name == null)
        throw new ArgumentNullException(nameof(name));
    return $"Hello {name}";
}

Pourra être simplifié en ceci :

public string SayHello(string name!) => $"Hello {name}";

Le point d'exclamation après le nom du paramètre ajoute automatiquement le null-check pour ce paramètre.

Améliorations du pattern matching

C# 9 apporte quelques améliorations bienvenues au pattern matching. La plus utile à mon avis est le pattern not, qui permet d'écrire ce genre de choses :

if (foo is not null) { ... }
if (animal is not Dog) { ... }

Les opérateurs relationnels (<, >=, etc.) et logiques (and et or) peuvent aussi être utilisés dans le pattern matching :

return size switch
{
    < 10 => "small",
    >= 10 and < 100 => "medium",
    _ => "large"
}

Les types record et les expressions with

C'est à mon avis la plus importante feature de C# 9. Créer un simple type de données en C# a toujours été plus laborieux que cela ne devrait ; il faut créer une classe, déclarer des propriétés, ajouter un constructeur si on veut que le type soit immuable, redéfinir Equals et GetHashCode, éventuellement ajouter un déconstructeur, etc. Au final, ça fait pas mal de code de plomberie pour obtenir des fonctionnalités assez basiques. Par exemple, une simple classe représentant un point pourrait actuellement ressembler plus ou moins à ça :

public class Point : IEquatable<Point>
{
    public Point(int x, int y) =>
        (X, Y) = (x, y);

    public int X { get; }

    public int Y { get; }

    public bool Equals(Point p) =>
        (p.X, p.Y) == (X, Y)

    public override bool Equals(object other) =>
        other is Point p && Equals(p);

    public override int GetHashCode() =>
        (X, Y).GetHashCode();

    public void Deconstruct(out int x, out int y) =>
        (x, y) = (X, Y);
}

Les types record en C# 9 vont rendre tout ça beaucoup plus simple ! En utilisant un type record, le code ci-dessus peut être résumé à ceci :

public data class Point(int X, int Y);

Eh oui, une seule ligne, et même pas bien longue ! Et cela inclut toutes les fonctionnalités du code précédent. Sympa, non ? Il est également possible de faire la même chose avec une structure plutôt qu'une classe, si un type valeur est plus approprié.

Une chose à noter : les types record sont immuables, on ne peut pas modifier les valeurs de leurs propriétés. Donc si on veut modifier une instance d'un type record, il faut en fait en créer une nouvelle (c'est le même principe qu'avec les dates ou les strings, par exemple). L'approche "classique" pour faire ça ressemblerait à ceci :

Point p1 = new Point(1, 2);
Point p2 = new Point(p1.X, 3);

Grosso modo, on copie toutes les propriétés de l'objet d'origine, sauf celles qu'on veut modifier. Dans cet exemple, ce n'est pas un problème car il n'y a que deux propriétés, mais s'il y en avait plus ça deviendrait vite laborieux.

C# 9 introduit les expressions with, qui permettent de faire ceci :

Point p1 = new Point(1, 2);
Point p2 = p1 with { Y = 3 };

Une expression with crée un clone de l'objet d'origine, en remplaçant les valeurs des propriétés spécifiées entres les accolades.

Il y a quelques autres features liées aux types records, comme les propriétés init-only, mais je ne vais pas toutes les couvrir ici. Si ça vous intéresse, lisez le billet de Mads Torgersen pour une présentation plus approfondie.

Target-typed conditionals

Il y a une petite chose qui agace les développeurs C# depuis bien longtemps : quand on utilise l'opérateur conditionnel (aussi appelé opérateur ternaire), les deux "branches" doivent avoir le même type, ou du moins il doit exister une conversion de l'une vers l'autre. Si bien que le code suivant ne compile pas :

Stream s = inMemory ? new MemoryStream() : new FileStream(path);

En effet, il n'existe pas de conversion entre MemoryStream et FileStream, et le compilateur ne sait donc pas déterminer le type de l'expression dans son ensemble. Pour corriger ça, il faut explicitement convertir une branche ou l'autre en Stream.

En C# 9, le code ci-dessus sera enfin valide ! Il faut juste que les deux branches soient convertibles vers le type de destination.

Covariance du type de retour

Actuellement, quand on redéfinit une méthode virtuelle d'une classe de base, la méthode redéfinie doit avoir le même type de retour que celle de la classe de base. Dans certaines situations, il serait plus pratique de pouvoir renvoyer un type plus spécifique. C# 9 rend cela possible, en permettant à la méthode redéfinie de renvoyer un type dérivé du type de retour de la méthode de base :

public class Thing
{
    public virtual Thing Clone() => new Thing();
}

public class MoreSpecificThing : Thing
{
    // Override with a more specific return type
    public override MoreSpecificThing Clone() => new MoreSpecificThing();
}

Instructions de niveau racine

Cette feature vise à réduire le code "boilerplate" pour les programmes simples. Actuellement, même le programme le plus simple a besoin d'une classe avec une méthode Main :

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello world");
    }
}

Tout ce code n'apporte pas grand chose d'utile, et complique l'apprentissage pour les débutants, car il oblige à appréhender tout de suite la notion de classe et de méthode.

En C# 9, il sera possible d'omettre la classe Program et la méthode Main, ce qui permettra de simplifier le code précédent comme ceci :

using System;
Console.WriteLine("Hello world");

Conclusion

La plupart des nouvelles features de C# 9 sont plutôt petites, conçues pour rendre le code plus simple, plus concis et plus lisible; elles sont très pratiques, mais ne changeront sans doute pas fondamentalement notre façon de coder. Les types record, en revanche, sont à mon avis une avancée importante. Ils facilitent énormément l'écriture de types de données immuables, ce qui, je l'espère, encouragera les développeurs à tirer parti de l'immutabilité partout où c'est possible.

Notez que C# 9 ne sortira pas avant quelques mois, et que beaucoup de choses peuvent encore changer d'ici là. Il est possible que certaines features mentionnées dans cet article soient modifiées, reportées à une version ultérieure, voire annulées.

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus