Fonctionnalités C# 8.0


Le but de cet article est de résumer et d’expliquer les fonctionnalités de C# 8.0. Dans un premier temps, on explicitera le contexte de C# 8 par rapport aux différents frameworks qui permettent de l’utiliser. Ensuite, on rentrera dans le détail des fonctionnalités.
Les fonctionnalités les plus rapides à expliquer se trouvent dans cet article. Les autres fonctionnalités nécessitant davantage d’explications se trouvent dans des articles séparés.

Précisions sur les versions de C#

Depuis C# 7, l’environnement .NET s’est étauffé avec .NET Core. Du code C# peut, ainsi, être compilé à partir de plusieurs frameworks. A partir de C# 8.0, l’environnement historique du framework .NET commence à être remplacé par .NET Core. Ainsi, certaines fonctionnalités de C# 8.0 ne sont pas disponibles dans le framework .NET mais seulement dans .NET Core. Le but de cette partie est d’expliciter les versions des composants .NET en relation avec C# 8.0.

Chronologie des releases

Ce tableau permet de résumer les dates de sorties de C# 8.0, de Visual Studio, du compilateur Roslyn, des versions du framework .NET et de .NET Core.

Date Version C# Version Visual Studio Compilateur Version Framework .NET Version .NET Core
Mai 2018 C# 7.3 VS 2017 (15.7) Roslyn 2.7/2.8 .NET 4.7.2
(NET Standard 1.0⇒2.0)
.NET Core 2.1
(NET Standard 1.0⇒2.0)
Aout 2018 VS 2017 (15.8) Roslyn 2.9
Novembre 2018 VS 2017 (15.9) Roslyn 2.10 .NET Core 2.2
(NET Standard 1.0⇒2.0)
Avril 2019 VS 2019 (16.0) Roslyn 3.0 .NET 4.8
(NET Standard 1.0⇒2.0)
Mai 2019 VS 2019 (16.1) Roslyn 3.1
Aout 2019 VS 2019 (16.2) Roslyn 3.2
Septembre 2019 C# 8.0 VS2019 (16.3) .NET Core 3.0
(NET Standard 1.0⇒2.1)
Novembre 2019 VS2019 (16.4)
Décembre 2019 .NET Core 3.1
(NET Standard 1.0⇒2.1)
Mars 2020 VS2019 (16.5)
Mai 2020 VS2019 (16.6) Roslyn 3.7
Juillet 2020 VS2019 (16.7)
Novembre 2020 C# 9.0 VS2019 (16.8) Roslyn 3.8 .NET 5.0
(NET Standard 1.0⇒2.1)

Lien entre la version C# et le compilateur

Le tableau précédent permet d’indiquer la version de C# dans le contexte des frameworks de façon à avoir une idée des sorties des autres éléments de l’environnement .NET. Toutefois, la version de C# est liée à la version du compilateur C#. Le compilateur est ensuite livré avec Visual Studio (depuis Visual Studio 2017 15.3) ou avec le SDK .NET Core.

Le chemin du compilateur est lié au composant avec lequel il est livré:

  • Avec Visual Studio: par exemple pour Visual Studio 2019 Professional: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\Roslyn\csc.exe
  • Avec les Build tools: par exemple pour les Build Tools for Visual Studio 2019: C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\Roslyn\csc.exe
  • Avec le SDK .NET Core:
    • Sur Linux: /usr/share/dotnet/sdk/<version>/Roslyn/bincore/csc.dll
    • Sur Windows: C:\Program Files\dotnet\sdk\<version>\Roslyn\bincore\csc.dll

On peut connaître la version du compilateur en tapant:

csc.exe -help

On peut savoir quelles sont les versions de C# que le compilateur peut gérer en exécutant:

csc -langversion:? 

Limiter la version C# à compiler

Par défaut, les versions C# traitées par le compilateur sont:

  • Framework .NET: C# 7.3
  • .NET Core 3.x: C# 8.0
  • .NET Core 2.x: C# 7.3
  • .NET Standard 2.1: C# 8.0
  • .NET Standard 2.0: C# 7.3
  • .NET Standard 1.x: C# 7.3

On peut volontairement limiter la versions C# que le compilateur va traiter.

  • Dans Visual Studio:
    dans les propriétés du projet ⇒ Onglet Build ⇒ Advanced ⇒ Paramètre Language version.
  • En éditant directement le fichier csproj du projet et en indiquant la version avec le paramètre LangVersion:
    <Project Sdk="Microsoft.NET.Sdk"> 
        <PropertyGroup> 
            <OutputType>Exe</OutputType> 
            <TargetFramework>netcoreapp2.0</TargetFramework> 
            <LangVersion>8.0</LangVersion> 
        </PropertyGroup> 
    </Project> 
    

Fonctionnalités C# 8.0

Les fonctionnalités les plus basiques de C# 8.0 sont présentées dans cet article. Les autres fonctionnalités nécessitant davantage d’explications sont présentées dans d’autres articles:

C# 8.0 n’est pas supporté par le framework .NET

Officiellement C# 8.0 est supporté par les frameworks satisfaisant .NET Standard 2.1 c’est-à-dire .NET Core 3.0 et .NET Core 3.1. Ainsi comme le framework .NET satisfait au maximum avec .NET Standard 2.0, il ne permet pas officiellement de compiler du code C# 8.0.

Microsoft ne fait plus évoluer les fonctionnalités du CLR du framework .NET ce qui exclut les fonctionnalités nécessitant une modification du CLR. Pour les autres fonctionnalités qui ne concernent que des éléments de syntaxe, il est possible de les utiliser parfois avec quelques aménagements.

Les fonctionnalités directement compatibles avec .NET Standard 2.0 sont:

Ces fonctionnalités sont directement utilisables à condition de compiler du code C# 8.0. Par exemple, si on cible le .NET Standard 2.0 avec le SDK .NET Core en indiquant netstandard20 dans la cible du fichier .csproj:

<Project Sdk="Microsoft.NET.Sdk"> 
    <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFramework>netstandard20</TargetFramework>
    </PropertyGroup> 
</Project> 

On obtiendra une erreur de compilation indiquant que la fonctionnalité n’est pas disponible en C# 7.3 (car, par défaut, pour générer une assembly .NET Standard 2.0 le compilateur compile du code C# 7.3):

error CS8370: Feature 'unmanaged constructed types' is not available in C# 7.3.
  Please use language version 8.0 or greater.

Si on précise explicitement la version C#, l’erreur n’est plus générée à la compilation:

<Project Sdk="Microsoft.NET.Sdk"> 
    <PropertyGroup> 
        <OutputType>Exe</OutputType> 
        <TargetFramework>netstandard20</TargetFramework>
        <LangVersion>8.0</LangVersion>
    </PropertyGroup> 
</Project> 

D’autres fonctionnalités ne sont pas supportées toutefois il est possible de les utiliser en implémentant les types manquants:

La fonctionnalité “Méthode d’interface par défaut” (i.e. default interface members) n’est pas compatible car elle nécessite une modification du CLR.

Fonction locale statique

C# 7.0 a permis de déclarer une fonction à l’intérieur d’une autre fonction. Cette fonction locale permet d’accéder aux variables et arguments de la fonction parente:

IEnumerable<int> GetPositiveNumber(IEnumerable<int> numbers, bool strictComparison)
{
  return numbers.Where(n => isPositive(n));

  bool isPositive(int number)
  {
    return strictComparison ? number > 0 : number >= 0;
  }
}

A partir de C# 8.0, la fonction locale peut être statique pour ne pas avoir accès au contexte de la fonction parente:

IEnumerable<int> GetPositiveNumber(IEnumerable<int> numbers, bool strictComparison)
{
  return numbers.Where(n => isPositive(n, strictComparison));

  static bool isPositive(int number, bool isStrict)
  {
    return isStrict ? number > 0 ; number >= 0;
  }
}

Utilisation de using sans bloc de code

Avant C# 8.0, using devait obligatoirement être suivi d’un bloc de code:

using (<objet satisfaisant IDisposable>) 
{ 
  // Bloc de code
  // ... 
} 

C# 8.0 permet d’utiliser using sans bloc de code. La portée de l’objet concerné par using correspond au bloc de code dans lequel se trouve using. La méthode Dispose() sera exécutée à la sortie de ce bloc de code.

Par exemple, dans le cas d’une méthode:

public void UseDisposableObject() 
{ 
  using var disposableObject = new DisposableObject(); 

  // Utilisation de disposableObject 
  // ... 

  // disposableObject.Dispose() est exécuté juste avant la sortie de la méthode 
} 

Dans le cas d’un bloc de code explicite:

public void UseDisposableObject() 
{ 
  {
    using var disposableObject = new DisposableObject(); 
    // Utilisation de disposableObject 
    // ...

    // disposableObject.Dispose() est exécuté juste avant la sortie du bloc 
  } 

  // A ce niveau disposableObject est hors de portée 
}

Méthode d’interface par défaut

Il est désormais possible, à partir de C# 8.0, de fournir une implémentation par défaut d’une méthode au niveau d’une interface, par exemple:

public interface IQuadrangle
{
  int Length { get; }

  int Width { get; }

  int GetArea()
  {
    return this.Length * this.Width;
  }
}

L’implémentation de la méthode GetArea() se trouve directement au niveau de l’interface IQuadrangle. Dans cette méthode, il est possible d’accéder à des propriétés déclarées dans l’interface comme Length et Width.

Pour rendre la lecture plus facile, on peut utiliser la version réduite de la syntaxe d’une méthode (disponible à partir de C# 6.0):

public interface IQuadrangle
{
  // ...
  int GetArea() => this.Length * this.Width;
}

Les règles liées à l’implémentation d’une méthode dans une interface sont différentes de celles appliquées dans le cas d’un héritage: la méthode n’est accessible que pour les variables dont le type est celui de l’interface. Cela signifie que:

  • La méthode n’est pas accessible si une variable est d’un type différent de l’interface.
  • Il n’y a pas de règles liées à l’héritage en utilisant new ou override.

Par exemple, si on considère la classe suivante satisfaisant IQuadrangle:

public class Rectangle : IQuadrangle
{
  public Rectangle(int length, int width)
  {
    this.Length = length;
    this.Width = width;
  }

  public int Length { get; }
  public int Width { get; }
}

Accessible avec le type de l’interface

On peut utiliser la méthode si la variable est du type de l’interface:

IQuadrangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // OK

Par contre si on considère le type de la classe, la méthode n’est pas accessible:

Rectangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // ERREUR: 'Rectangle' does not contain a definition for 'GetArea'.

Modifier l’implémentation dans la classe

On peut réimplémenter la méthode dans la classe. L’implémentation dans la classe sera utilisée en priorité:

public class Rectangle : IQuadrangle
{
  // ...

  public int GetArea()
  {
    Console.WriteLine("From Rectangle");
    return this.Length * this.Width;
  }
}

Quelque soit le type de la variable, l’implémentation utilisée sera celle de la classe:

IQuadrangle quad = new Rectangle(2, 3);
int area = quad.GetArea(); // From Rectangle

Rectangle rect = new Rectangle(2, 3);
int area = rect.GetArea(); // From Rectangle

Accéder à la méthode dans la classe

Accéder à la méthode dans la classe n’est pas direct car la méthode n’est accessible que si on considère le type de l’interface, par exemple:

public class Rectangle : IQuadrangle
{
  // ...

  public int AddToRectangleArea(int otherArea)
  {
    return otherArea + GetArea(); // ERREUR: GetArea() n'est pas accessible dans la classe si elle n'est pas réimplémentée dans la classe
  }
}

Pour accéder à la méthode, il faut considérer l’interface:

public class Rectangle : IQuadrangle
{
  // ...

  public int AddToRectangleArea(int otherArea)
  {
    return otherArea + ((IQuadrangle)this).GetArea(); // OK
  }
}

Implémenter une méthode statique dans l’interface

Les méthodes statiques sont supportées par cette fonctionnalité:

public interface IQuadrangle
{
  int Length { get; }

  int Width { get; }

  static int GetArea(IQuadrangle quadrangle)
  {
    return quadrangle.Length * quadrangle.Width;
  }
}

La méthode étant statique, ne permet pas d’accéder aux propriétés instanciées de l’interface.

Les mêmes règles s’appliquent quant à l’accès de la méthode statique à l’extérieur ou dans une classe satisfaisant l’interface: la méthode n’est accessible que si on considère le type de l’interface. Cependant comme la méthode est statique, l’accès à la méthode se faire en considérant le type de l’interface:

  • A l’extérieur:
    Rectangle rect = new Rectangle(2, 3);
    int area = IQuadrangle.GetArea(rect);
    
  • A l’intérieur de la classe:
    public class Rectangle : IQuadrangle
    {
      // ...
    
      public int GetRectangleArea()
      {
        Console.WriteLine("From Rectangle");
        return IQuadrangle.GetArea(this);
      }
    }
    

Index et plage d’une liste

A partir de C# 8.0, 2 nouveaux types sont supportés par les structures de données de type liste comme System.Array ou les listes génériques:

Le support de ces types par les listes permet de gérer davantage de cas de figure.

System.Index

Cette structure permet de stocker l’index d’une liste à partir du début ou de la fin de la liste en commençant par 0, par exemple:

  • Index à partir du début d’une structure:
    Index index = new Index(2);
    // ou
    Index index = new Index(2, false);
    
  • Index à partir de la fin d’une structure:
    Index index = new Index(2, true); // 2e valeur en partant de la fin
    Index index = new Index(0, true); // ERREUR: le premier index en partant de la fin est 1
    

    Une autre notation possible:

    Index index = ^1; // Dernière valeur de la liste
    Index index = ^2; // 2e valeur en partant de la fin
    Index index = ^0; // ERREUR
    

L’index s’utilise avec une liste:

var array = new int[]{ 0, 1, 2, 3, 4, 5 };
var index = ^1;
int value = array[index];
// ou plus directement
value = array[^1];

System.Range

Cette nouvelle structure est une plage d’index pouvant être utilisée avec une liste. Cette plage comprend un index de début et un index de fin. Utilisée avec une liste, la plage permet d’obtenir une autre liste dont les valeurs correspondent à la plage d’index.

L’index de fin de la plage est exclusif

L’index de fin est exclusif, cela signifie qu’il ne fait pas partie de la plage d’index.
Si on considère une liste d’entiers et la plage d’index suivantes:

var values = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
Range range = new Range(0, 3); // Plage de la 1ère à la 3e valeur

values[range] contient les valeurs 'A', 'B' et 'C'. 'D' ne fait pas partie des valeurs de la plage car la plage est définie avec:

  • 0 en tant qu’index de début et
  • 3 en tant qu’index exclusif de fin.

Plusieurs syntaxes sont possibles pour instancier un objet Range:

  • En utilisant la syntaxe courte des index:
    Range range = new Range(2, ^1); // Plage de la 3e à l'avant dernière valeur 
    range = new Range(2, ^0); // Plage de la 3e à la dernière valeur 
    
  • Avec un index ou plusieurs objets de type Index:
    Index startIndex = new Index(0);
    Index endIndex = new Index(2);
    Range range = new Range(0, endIndex);
    range = new Range(startIndex, 2);
    range = new Range(startIndex, endIndex);
    
  • Les plages peuvent utilisées une syntaxe courte:
    Range range = 0..2;
    range = 0..endIndex;
    range= startIndex..endIndex;
    range = 2..^0; // Plage de la 3e à la dernière valeur
    range = 2..^1; // Plage de la 3e à l'avant dernière valeur
    

Les objets Range s’utilise directement avec les listes:

var values = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
Range range = 2..^0;
char[] subSet = values[range];

subSet contient les valeurs de values de la 3e à la dernière valeur: 'C', 'D', 'E' et 'F'.

Autre exemple:

range = 2..^1;
subSet = values[range];

subSet contient les valeurs de values de la 3e à l’avant dernière valeur: 'C', 'D' et 'E'.

Une exception System.ArgumentOutOfRangeException est lancée si la plage est en dehors de valeurs disponibles dans la liste:

Range range = 2..7;
char[] subSet = values[range]; // ERREUR: la liste values contient 6 valeurs, le 7e index n'existe pas. 

Si la liste contient les index de la plage:

Range range = 2..6;
char[] subSet = values[range]; // OK

subSet contient les valeurs du 2e index au 5e index (l’index de fin est exclusif): 'C', 'D', 'E' et 'F'.

Si la plage ne permet pas de renvoyer des valeurs alors une exception est lancée:

Range range = 7..^1; // Plage de la 8e à l'avant dernière valeur
char[] subSet = values[range]; // ERREUR: values ne contient que 6 valeurs, l'index 7 n'existe pas.

Amélioration des chaines de caractères textuelles interpolées

Cette fonctionnalité permet de déclarer des chaînes de caractères textuelles interpolées avec $@"..." et @$"...". Avant C# 8.0, seule la syntaxe $@"..." était possible.

Ainsi:

int fileCount = 2;
string interpolatedString = $@"C:\MyRepo contient {fileCount} fichiers.";

est équivalent à:

string interpolatedString = @$"C:\MyRepo contient {fileCount} fichiers.";

Pour rappel, une chaîne de caractères textuelle interpolée correspond à 2 fonctionnalités:

  • Une chaîne de caractères textuelle (i.e. verbatim string literal): déclarée en la préfixant avec @"...". Ce type de chaîne permet d’indiquer un contenu dans lequel il n’est pas nécessaire d’échapper certains caractères spéciaux comme \ (i.e. antislash), retour chariot \r (i.e. carriage return) ou saut de ligne \n (i.e. line feed). Ces caractères sont interprétés directement, par exemple:
    • Avec le caractère \: pour déclarer une chaîne de caractères contenant C:\CustomFolder\InnerFolder\, on peut utiliser la syntaxe "C:\\CustomFolder\\InnerFolder\\" ou @"C:\CustomFolder\InnerFolder\".
    • Avec les caractères \r (i.e. carriage return) et \n (i.e. line feed): pour déclarer une chaîne contenant:
      Retour
      à
      la
      ligne
      

      On peut utiliser la syntaxe: Retour\r\nà\r\nla\r\nligne ou plus directement avec une chaîne textuelle:

      @"Retour
      à
      la
      ligne"
      

    Avec une chaîne de caractères textuelles, le caractère " peut être échappé avec "" (dans le cas d’une chaîne normale, il faut utiliser \").

  • Une chaîne de caractères interpolée: permet de déclarer une chaîne en évaluant une expression entre les caractères {...} par exemple $"La date du jour est: {DateTime.Now}".

    Cette syntaxe permet d’autres raccourcis comme:

    1. Permettre d’aligner des chaînes en indiquant un nombre minimum de caractères avec la syntaxe {<expression>,<nombre de caractères>}:
      • Si le nombre de caractères d’alignement > 0 ⇒ des espaces sont rajoutés à gauche, par exemple:
        int result = 2;
        Console.WriteLine($"Le résultat est: '{result,5}'.");
        

        L’affichage est:

        Le résultat est: '    2'.
        
      • Si le nombre de caractères d’alignement < 0 ⇒ des espaces sont rajoutés à droite, par exemple:
        int result = 2;
        Console.WriteLine($"Le résultat est: '{result,-5}'.");
        

        L’affichage est:

        Le résultat est: '2    '.
        
    2. Formatter une chaîne en utilisant la syntaxe {<expression>:<formattage de la chaîne>}, par exemple:
      DateTime now = DateTime.Now;
      string syntax1 = $"La date du jour est: {now.ToString("dd/MM/yyyy")}.";
      string syntax2 = $"La date du jour est: {now:dd/MM/yyyy}.";
      

      Le contenu de syntax1 et syntax2 est le même:

      La date du jour est: 05/02/2021.
      

      Une liste exhautive des possibilités de formattage d’une chaîne se trouve sur: docs.microsoft.com/fr-fr/dotnet/standard/base-types/composite-formatting.

    Pour échapper les caractères { et } dans une chaîne interpolée, il faut utiliser {{ et }}.

Autres fonctionnalités

Les autres fonctionnalités sont traitées dans d’autres articles:

Références

Leave a Reply