Nouveautés C# 8.0

Quelles nouveautés nous propose C# 8 ? Si ce vous disais que c’était dément, un must-have, vous en diriez quoi ?

Je vous propose dans cet article de détailler les fonctionnalités que je trouve les plus intéressantes pour le programmeur.

Nous allons commencer par une fonctionnalité qui nous vient de Java, des méthodes par défaut sur les interfaces. Quel est l’intérêt ? Proposer un fonctionnement de base à la simple implémentation d’une interface. Proposer des méthodes par défaut, c’est aussi proposer des membres statiques (champs et méthodes) dans les interfaces. Mais rien de mieux qu’un petit exemple pour comprendre :

 interface IProduit
 {
     private static decimal tauxTVA = 20.6M;

     public static void SetTauxTVA(decimal taux)
     {
          tauxTVA = taux;
     }

     public decimal PrixHT { get; }

     public decimal CalculPrixTTC() => DefautCalcul(this);

     protected static decimal DefautCalcul(IProduit p)
     {
         return p.PrixHT + p.PrixHT * tauxTVA / 100;
     }
 }

Membres Readonly

On peut désormais appliquer un modifier readonly sur un membre d’une structure. L’intérêt est d’indiquer qu’un membre ne modifie pas l’état de la classe.

public struct Point
{
     public double X { get; set; }
     public double Y { get; set; }
}

public struct Line
{
     Point A;
     Point B;
     public readonly double Distance => Math.Sqrt(Math.Pow(A.X - B.X, 2) + Math.Pow(A.Y - B.Y, 2));
 }

Notez que le readonly implique une rigueur dans la chaîne des appels. Imaginons la situation suivante, on rajoute une propriété « multiplicateur » en mode explicite et que l’on modifie le calcul de distance.

public struct Line
{
    Point A;
    Point B;

    double multiplicateur;

    public double Multiplicateur
    {
        get
        {
            return multiplicateur;
        }

        set
        {
            multiplicateur = value;
        }
    }

    public readonly double Distance => Multiplicateur * Math.Sqrt(Math.Pow(A.X - B.X, 2) + Math.Pow(A.Y - B.Y, 2));

Le compilateur nous renvoie un warning « L’appel au membre ‘Line.Multiplicateur.get’ non readonly à partir d’un membre ‘readonly’ génère une copie implicite de ‘this' »

Cette erreur est logique car la propriété get de Multiplicateur n’est pas considérée readonly par défaut. Il faudrait écrire :

  public double Multiplicateur
    {
        readonly get
        {
            return multiplicateur;
        }

        set
        {
            multiplicateur = value;
        }
    }

Notez qu’il n’y aurait pas de message d’information si l’on écrivait

public double Multiplicateur {get;set;}

En effet, si la propriété est implémentée implicitement le compilateur peut garantir la lecture seule.

Méthodes d’interface par défaut

Ce n’est pas une fonctionnalité que vous devriez utiliser tous les jours, mais elle est sympathique pour ceux qui créer des API. Cette fonctionnalité nous vient directement de Java et a été mis en oeuvre dès la version 8 du langage/plateforme JDK.

Est-ce utile ? Complètement.

Pour bien comprendre les méthodes d’interface par défaut, il faut penser dans une logique fournisseur API/Client avec des équipes séparées. Si vous développez une application « monolithique » en termes d’équipe, de logique d’implémentation ou déploiement (une application où tout le monde a accès en même temps aux sources, une application où des équipes séparées travaillent sur le même produit, sont à proximité ou déploie tout en même temps), cette problématique n’existe pas généralement pas car il vous est aisé de vous coordonner. En revanche, si vous fournissez/vendez une API à des clients, que cette API demande l’implémentation d’une interface, vous étiez jusqu’alors bloqué en terme d’extension. En effet, ajouter des méthodes sur une interface existante oblige les clients qui l’utilise à modifier leur code, autrement dit à implémenter de nouvelles méthodes, en cas d’ajout de nouvelles fonctionnalité. Il existe des alternatives, comme ne pas modifier l’interface et ajouter des méthodes Helpers mais c’est casser la véritable logique de son code, d’autant plus que les clients n’auront pas le réflexe immédiat d’aller voir ces Helpers (il les verront comme des méthodes utilitaires et non des fonctionnalités « core ». Or elles sont pour vous essentielles en terme de design).

De façon plus pragmatique, outre la création d’API, cette évolution de C# facilite également l’interaction avec des API qui ont pour cible Android (Java) ainsi qu’iOS (Swift) qui supportent déjà ces méthodes. Enfin, cette évolution est considérée comme un « trait« , c’est à dire une technique pour ajouter des fonctionnalités à une classe pour étendre ses fonctionnalités.

La mise en oeuvre est simple, mais pour des raisons de cohérence je préfère ne pas vous les montrer dans cet article et vous renvoyez directement vers la documentation de Microsoft. En effet, dans un mode de développement classique ce n’est pas quelque chose que vous ferez souvent. Ensuite, il ne faut pas perdre de vue toutes les autres possibilités, comme créer de nouvelles interfaces, avoir une classe abstraite ou créer des méthodes d’extension.

Déclaration des using et Asynchronous disposable

Une fonctionnalité gadget mais néanmoins sympathique qui évite l’utilisation des accolades lors d’un using.

void CreateFile(string file, List<string> lines)
{
      using var writer= new System.IO.StreamWriter(file);
      foreach (string line in lines)
      {
         writer.WriteLine(line);
      }
}

Si vous observez le code ci-dessus, les classiques accolades ont disparu. D’ordinaire vous auriez écrit !

void CreateFile(string file, List<string> lines)
{
      using (var writer= new System.IO.StreamWriter(file))
      {
         foreach (string line in lines)
         {
            writer.WriteLine(line);
          }
      }
}

Le réel avantage de cette fonctionnalités réside non seulement dans les économies d’accolades et parenthèses mais également dans les variables intermédiaires qu’il fallait autrefois prendre parfois soin de déclarer en dehors du using.

Ce n’est pas tout, using peut désormais gérer des types qui implémentent IAsyncDisposable. Comme sont nom n’indique, cela permet la gestion asynchrone de la libération de ressources. Le compilateur génère un code qui attend (await) la tâche retournée par AsyncDisposable.DisposeAsync lors d’un dispose.

Fonctions locales static

Les fonctions locales sont bien pratiques. Elles permettent de clarifier un code en évitant la création de « sous-fonctions » au niveau d’une classe alors qu’elles ne appelées que par une seule et unique méthode. Le code gagne en lisibilité et tout le monde pourrait être heureux. Mais il y a un hic qui peut être pervers : en déclarant une fonction dans une méthode on peut aisément capturer les variables dans le scope et involontairement les utiliser.

Imaginons la méthode suivante (ne reproduisez pas ce code chez vous, il sert juste d’exemple. Il y a évidemment plus simple pour faire la somme de 3 nombres):

 static int Add(int x, int y, int z)
 {
     int calcul;
     FonctionLocale(x,y);
     FonctionLocale(calcul, z);
     return calcul;

     void FonctionLocale(int x, int y) => calcul = x + z;
 }

Dans le code ci-dessus, le programmeur s’est trompé dans sa fonction locale, il voulait écrire :

     void FonctionLocale(int x, int y) => calcul = x + y;

Il a remplacé le « y » par un « z ». Lors de l’écriture d’une fonction (j’utilise ici le mot fonction pour insister sur le fait que ce n’est pas une méthode de classe mais bien un regroupement de lignes de code ponctuel pour l’usage unique d’une méthode) en mode classique, c’est à dire sous la forme d’une méthode privée de classe, le problème ne se serait pas posé puisque la variable « z » n’aurait pas été définie et le compilateur nous aurait prévenu.

L’idée de fonction locale static est de pouvoir utiliser le mot clé static pour éviter ce type de désagrément. Un programmeur peut protéger les fonctions locales qui ne doivent pas capturer le scope de la méthode.

L’ajout de static devant notre fonction locale entraîne des messages d’erreur signalant que ni la variable « calcul », ni la variable « z » ne sont accessibles. Le programmeur doit mettre à jour sa méthode de la façon suivante :

 static int Add(int x, int y, int z)
 {
     int calcul;
     calcul = FonctionLocale(x,y);
     calcul = FonctionLocale(calcul, z);
     return calcul;

     static int FonctionLocale(int x, int y) =>  x + y;
}

Streams asynchrones

Je trouve cette fonctionnalité des plus pratiques qu’il soit : pouvoir créer et consommer des streams de façon asynchrone !

Oui, avec la mécanique bien connue des async/await et des yield appropriés, nous pouvons faire des streams asynchrone. Tout réside dans l’emploi adéquat du type IAsyncEnumerable et des méthodes qui retourne des Task/ValueTask.

Voici un exemple de génération asynchrone de 200 éléments avec une pose de 150 ms entre chaque item

public static async IAsyncEnumerable<int> StreamSequence()
{
    for (int i = 0; i < 200; i++)
    {
        await Task.Delay(150);
        yield return i;
    }
}

Voici un code asynchrone de consommation

await foreach (var number in StreamSequence())
{
    Console.WriteLine(number);
}

Null-coalescing assignment

Un nouvel opérateur  » ??= » permet d’attribuer une valeur à une variable si cette dernière est nulle. Voici un petit exemple :

int? i = null;
i ??= 5;
i ??= 7;

Console.WriteLine(5);  // écrit 5

Index et plages de valeurs

Il est désormais plus facile d’accéder à un élément dans une séquence grâce aux Index, de même qu’il est plus facile de gérer une plage de valeurs grâce aux Range .

Avec les index on peut désormais accéder à un élément par la fin grâce à l’usage d’une écriture de type ^n comme dans l’exemple suivant

var maListe= new string[]
{
   "voiture",
   "maison",
   "moto",
   "hololens",
   "réalité mixte",
   "réalité virtuelle"
};  
Console.WriteLine($"Le dernier mot est {maListe[^1]}");

Grâce aux plages de valeurs, on peut manipuler aisément un sous-ensemble de données. Avec l’exemple ci-dessous on peut écrire :

var reality = maListe[^3 ..^ 0]; // hololens... réalité virtuelle
var debut = maListe[..3]; //début jusqu'à moto
var fin = maListe[4..]; //réalité mixte jusqu'à la fin

Switch amélioré

Pour un soucis d’efficacité, la gestion des switch est facilité pour aller plus vite dans l’écrite en positionnant le nom de la variable avant le switch et en utilisant non seulement des lambdas pour les prises de décisions, mais en pouvant également décider en fonction des propriétés de l’objet que l’on manipule dans le switch comme dans l’exemples ci-dessous :

public enum TVAType { TauxNormal, TauxPremiereNecessite,TauxAgricole}

public class TVAElem
{
     public string Nom{get;set;}
      public TVAType TVA { get; set; }
}

public static decimal CalculTaxe(decimal prixHT, TVAElem tvaElement) =>
    tvaElement switch {
 { Nom: "Type 1", TVA: TVAType.TauxNormal } => prixHT * 1.20M, //Taux 20%
 { Nom: "Type 1", TVA: TVAType.TauxPremiereNecessite } => prixHT * 1.055M, //Taux 5.5%
 { Nom: "Type 1", TVA: TVAType.TauxAgricole } => prixHT * 1.10M, //Taux 10%
 // Autres cas
  _ => throw new ArgumentException("Calcul impossible")};

Cet article est une présentation des fonctionnalités que je trouve essentiel, avec C# 8 mais il y en a d’autres.

Vous pouvez accéder à la liste complète ici.