Les fonctionnalités C# 9.0


Le but de cet article est de résumer et d’expliquer les fonctionnalités de C# 9.0. Dans un premier temps, on explicitera le contexte de C# 9.0 par rapport aux autres composants (frameworks, IDE, compilateur etc…) 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# 8.0, les évolutions fonctionnelles de .NET se font pour .NET Core seulement. Le framework .NET est toujours supporté toutefois les nouvelles fonctionnalités ne sont pas implémentées pour cet environnement.

Etant donné que l’environnement correspondant au framework .NET n’évoluera plus, l’environnement .NET Core a été renommé .NET. Ainsi .NET 5.0 correspond à la nouvelle version uniformisée de .NET.

Comme les environnements framework .NET et .NET Core ne subsistent plus en parallèle, l’approche .NET Standard n’a plus d’intérêt. .NET Standard s’arrête donc à .NET 5.0.

Chronologie des releases

Ce tableau permet de résumer les dates de sorties de C# 9.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
Septembre 2019 C# 8.0 VS2019 (16.3) Roslyn 3.2(1) .NET 4.8(2)(3)
(NET Standard 1.0⇒2.0)
.NET Core 3.0
(NET Standard 1.0⇒2.1)
Novembre 2019 VS2019 (16.4)
Décembre 2019 .NET Core 3.1(4)
(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)(5)
Février 2021 VS2019 (16.9) Roslyn 3.9
Mai 2021 VS2019 (16.10) Roslyn 3.10
Août 2021 VS2019 (16.11)
Novembre 2021 C# 10.0 VS2022 (17.0) .NET 6.0
  • (1): Roslyn 3.2 est sorti en août 2019
  • (2): Le framework .NET 4.8 est sorti en avril 2019
  • (3): .NET 4.8 est la dernière version du framework .NET. Les nouvelles fonctionnalités ne seront plus développées dans cet environnement.
  • (4): La dénomination .NET Core est remplacée par .NET. L’environnement correspondant au framework .NET s’arrête à la version 4.8. Les versions .NET 5.0 et supérieurs correspondent à l’environnement .NET Core.
  • (5): .NET Standard n’est plus nécessaire puisque les 2 environnements framework .NET et .NET Core n’existent plus. Ils ont laissé place à l’environnement uniformisé .NET (voir .NET 5+ and .NET Standard pour plus de détails).

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:

  • .NET 5.0: C# 9.0
  • 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>net5.0</TargetFramework> 
            <LangVersion>9.0</LangVersion> 
        </PropertyGroup> 
    </Project> 
    

Fonctionnalités C# 9.0

Les fonctionnalités les plus basiques de C# 9.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:

Accesseur init

Avec C# 9, l’accesseur init est ajouté à la syntaxe C#. Cet accesseur permet de limiter l’affectation d’une propriété d’un objet dans le corps du constructeur, dans un initializer ou avec le mot-clé with (valable pour les records).

Pour une version antérieure à C# 9, les accesseurs possibles d’une propriété sont:

  • get: accesseur en lecture pour accéder à la valeur d’une propriété d’un objet
  • set: accesseur en écriture pour affecter une valeur à une propriété

Accesseurs get et set

L’absence de l’accesseur set permet de réserver l’affectation d’une propriété au constructeur, par exemple si on considère la classe suivante:

public class Car
{
  public string Brand { get; set; }
  public string Model { get; set; }

  public Car()
  {
    this.Brand = "Ford"; // Affectation possible dans le constructeur
  }
}

Il est possible d’affecter une valeur aux propriétés à tout niveau:

var car = new Car();
car.Brand = "Ford";
car.Model = "Mustang";

En l’absence d’accesseur set, l’affectation n’est possible que dans le constructeur:

public class Car
{
  public string Brand { get; }
  public string Model { get; set; }

  public Car()
  {
    this.Brand = "Ford"; // OK
  }
}

// ...

var car = new Car();
car.Brand = "Renault"; // ⚠ ERREUR ⚠ car non accessible en écriture
car.Model = "Mustang"; // OK

En l’absence d’accesseur en écriture, l’affectation n’est pas possible dans un initializer:

var car = new Car { Brand = "Ford", Model = "Mustang" }; // ⚠ ERREUR ⚠ il n’est pas possible d’affecter Brand

Les accesseurs sont valables pour les classes, les records et les structures.

Syntaxe non condensée
Généralement les accesseurs sont écrits sous une forme condensée:

public class Car
{
  public string Brand { get; set; }
}

L’équivalent plus verbeux de cette syntaxe condensée est:

public class Car
{
  private string brand;

  public string Brand 
  {
    get { return this.brand; }
    set { this.brand = value; }
  }
}
C# 9.0

Nouvel accesseur init

C# 9 introduit l’accesseur init qui autorise l’affectation dans le constructeur et dans un initializer, par exemple:

public class Car
{
  public string Brand { get; init; }
  public string Model { get; init; }

  public Car(string brand, string model)
  {
    this.Brand = brand; // OK
    this.Model = model; // OK
  }

  public Car() {} // Constructeur par défaut pour permettre l’utilisation d’un initializer
}

Les propriétés peuvent être affectées en utilisant un initializer:

var car = new Car { Brand = "Ford", Model = "Mustang" }; // OK

En revanche les affections en dehors du constructeur et de l’initializer ne sont pas possibles:

car.Brand = "Renault"; // ⚠ ERREUR ⚠

Affectation possible à partir d’un autre accesseur init

Si une propriété comporte un accesseur init, il est possible d’affecter cette propriété dans le corps de l’accesseur d’une autre propriété. Par exemple, si on considère la classe suivante avec un accesseur utilisant une syntaxe non condensée:

public class Car
{
  private string brand;

  public string Brand 
  { 
    get { return this.brand; } 
    init { this.brand = value; }
  }
  public string Model { get; init; }
}

Il est possible d’affecter la propriété Model à partir de l’accesseur init de la propriété Brand:

public class Car
{
  private string brand;

  public string Brand 
  { 
    get { return this.brand; } 
    init { 
      this.brand = value; 
      this.Model = "Unknown"; // OK
    }
  }
  public string Model { get; init; }
} 

L’affectation est possible dans l’accesseur init d’une propriété d’une classe dérivée, par exemple:

public class Vehicle
{
  public string Brand { get; set; }
}

public class Car : Vehicle
{
  private string model;

  public string Model 
  {
    get { return this.model; }
    init { 
      this.model = value;
      this.Brand = "Unknown"; // OK
     } 
  }
}

Affectation possible dans le constructeur d’un type dérivé
Dans le cas des classes et des records (les structures n’autorisent pas l’héritage), il est possible d’effectuer des affectations dans le constructeur des objets dérivés.

Par exemple:

public class Vehicle
{
  public string Brand { get; init; }
}

public class Car : Vehicle
{
  public Car(string brand)
  {
    this.Brand = brand; // OK
  }
}

readonly
Comme pour set, l’accesseur init permet d’affecter des membres avec un opérateur readonly. Le mot-clé readonly peut être utilisé pour indiquer qu’un membre d’une classe ou d’une structure ne peut être initialisé que par un initializer ou par le constructeur.

Par exemple:

public class Car
{
  private readonly string brand;

  public string Brand {
    get { return this.brand; }
    init {
      this.brand = value; // OK
     }
  }
}

L’affectation d’un membre readonly n’est possible que dans la classe dans laquelle le membre est défini. Les classes dérivées ne peuvent pas affecter un membre readonly:

public class Vehicle
{
  protected readonly string brand;
}

public class Car: Vehicle
{
  public string Brand {
    get { return this.brand; }
    init {
      this.brand = value;  // ⚠ ERREUR ⚠: brand ne peut être affecté que dans Vehicle
    }
  }
}

Utilisation de with
A partir de C# 9, il est possible d’utiliser des objets de type record. Une méthode pour instancier ces objets est d’utiliser with. with permet de créer un nouvel objet record à partir d’un objet existant, par exemple si on considère le record suivant:

public record Car
{
  public string Brand { get; init; }
  public string Model { get; init; }
}

Si on utilise with pour créer un nouveau record:

var initialCar = new Car { Brand = "Renault", Model = "Clio" };
var littleCar = initialCar with { Model = "Twingo" };

Console.WriteLine(initialCar.Brand); // Renault
Console.WriteLine(initialCar.Model); // Twingo

Opérateurs de portée
Comme pour get et set, on peut ajouter un opérateur de portée à init pour modifier sa portée, par exemple:

public class Vehicle
{
  public string Brand { get; private init; }  // Pour limiter à la classe seulement
  public string Model { get; protected init; } // Pour limiter aux classes dérivées
}

Ainsi si on considère une classe dérivant de Vehicle

public class Car: Vehicle
{
  public Car(string brand, string model)
  {
    this.Brand = brand; // ⚠ ERREUR ⚠ à cause de private init
    this.Model = model; // OK
  }
}

new()

On peut omettre de préciser le type lors de l’instanciation d’un objet avec l’opérateur new quand le type est connu:

  • Avant C# 9:
    ExampleClass instance = new ExampleClass(arg1, arg2, arg3);
    
  • A partir de C# 9:
    ExampleClass instance = new(arg1, arg2, arg3); // Le type peut être omis après new
    

Pour utiliser new(), il faut que le compilateur puisse déterminer le type, l’utilisation de var n’est donc pas possible:

var instance = new(); // ⚠ ERREUR ⚠

Dans le cas où il n’y a pas d’arguments dans le constructeur, on utilise la forme new():

ExampleClass instance = new(); // OK

Le type peut être trouvé par le compilateur lors de la création:

  • D’une liste:
    List<ExampleClass> list = new() { new() }; 
    
  • D’un dictionnaire:
    Dictionary<string, string> dictionary = new()
    {
      { "key", "value" }
    };
    
  • D’un enum:
    Si on considère l’enum suivant:

    public. enum EnumExample
    {
      value1,
      value2,
      value3,
    }
    

Les formes suivantes sont équivalentes:

EnumExample enum1 = new EnumExample();
EnumExample enum2 = default; // A partir de C# 7.1
EnumExample enum3 = new();

Dans tous les cas, la valeur des enum est value1.

Utiliser new() est possible en retour d’une fonction:

Car CreateNewCar()
{
  return new();
}

On ne peut pas utiliser new() pour un tableau:

ExampleClass[] array = new ExampleClass[] {}; // OK
ExampleClass[] array = new() {}; // ⚠ ERREUR ⚠

Fonctions anonymes statiques

C# 9 permet de créer des fonctions anonymes statiques de façon à ne pas utiliser le contexte d’exécution.

Comme leur nom l’indique, les fonctions anonymes sont des fonctions dont la définition ne possède pas de nom en opposition aux fonctions classiques. Pour les utiliser, on définit des delegates qui sont des références vers la fonction. La définition d’un delegate correspond à une signature de fonction précise:

public delegate int ArithmeticOperation(int a, int b);

Le delegate permettra d’indiquer le type de la référence vers une fonction. Cette référence peut être créée à partir d’une fonction anonyme:

ArithmeticOperation multiply = delegate(int a, int b)
{
  return a * b;
};

A partir de C# 3, sont apparues les fonctions lambda qui permettaient de définir facilement des fonctions anonymes sans avoir à définir au préalable des delegates:

Func<int, int, int> multiply = (int  a, int b) => {
  return a * b;
};

Func<int, int, int> est un delegate dont la définition se trouve dans le framework:

public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

Pour exécuter la fonction anonyme, il suffit d’utiliser la référence:

int result = multiply(2, 3);

La fonction anonyme étant définie à l’intérieur d’une autre fonction, elle peut être une closure c’est-à-dire qu’elle peut capturer des variables provenant du contexte de cette autre fonction, par exemple:

public void ExecuteMe()
{
  int localVar = 0;
  // Fonction lambda sans argument
  Action printLocalVar = () => 
  {
    Console.WriteLine(localVar); // La variable localVar est capturée
  };

  printLocalVar(); // 0
  
  localVar++;

  printLocalVar(); // 1
}
C# 9.0

A partir de C# 9, il est possible de définir des fonctions anonymes statiques. Ces fonctions ne capturent pas le contexte extérieur. Pour définir une fonction anonyme statique, il faut utiliser le mot classique static:

ArithmeticOperation multiply = static delegate(int a, int b)
{
  return a * b;
};

Si on utilise une fonction lambda, de la même façon on peut utiliser le mot clé static pour rendre la fonction statique:

Func<int, int, int> multiply = static (int  a, int b) => {
  return a * b;
};

Si la fonction anonyme est statique, il n’est pas possible d’utiliser des variables dans une closure, les variables sont obligatoirement des arguments ou des variables définies localement dans le corps de la fonction anonyme. En reprenant l’exemple précédent:

public void ExecuteMe()
{
  int localVar = 0;
  // Fonction lambda sans argument
  Action<int> printLocalVar = static (arg) => 
  {
    Console.WriteLine(arg); 
  };

  printLocalVar(localVar); // 0
  
  localVar++;

  printLocalVar(localVar); // 1

}

Déclaration de premier niveau

Cette fonctionnalité permet de simplifier le code de la fonction Main() d’applications en permettant d’omettre la déclaration d’un namespace, d’une classe et d’une méthode Main().

Par exemple, si on crée une nouvelle application console avec:

dotnet new console

Dans le fichier Program.cs, au lieu d’écrire un Main de cette façon:

using System;

namespace SimpleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      int result = 0;
      int n1 = 0;
      int n2 = 1;

      for (int i = 0; i < 15; i++)
      {
        result = n1 + n2;
        n2 = n1;
        n1 = result;
        Console.WriteLine(result);
      }
    }
  }
}

On peut omettre le namespace, la classe, la méthode Main() et les déclarations using dans le fichier Program.cs:

int result = 0;
int n1 = 0;
int n2 = 1;
for (int i = 0; i < 15; i++)
{
    result = n1 + n2;
    n2 = n1;
    n1 = result;
    Console.WriteLine(result);
}

A la compilation, le contenu du fichier Program.cs sera considéré comme étant le Main. Toutefois il est possible dans le même fichier, de déclarer d’autres méthodes, classes ou namespaces. Une erreur de compilation sera générée si un autre fichier contient un Main():

namespace Example
{
  public class EntryPoint
  {
    static void Main(string[] args)
    {
      // ...
    }
  }
}

Cette fonctionnalité n’impose pas d’avoir qu’un seul fichier, on peut créer d’autres classes dans d’autres fichiers. En revanche si un autre fichier contient des déclarations sans indications de méthode, de classe ou de namespace, une erreur sera générée:

error CS8802: Only one compilation unit can have top-level statements. 

Expression conditionnelle vers un type cible

Une expression conditionnelle correspond à une expression ternaire du type:

<condition> ? <expression 1 si condition vraie> : <expression 2 si condition fausse>

Pour que cette expression soit valide, il faut qu’elle soit intégrée à une déclaration du type:

var result = <expression conditionnelle>;

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

public class A 
{
  public int InnerProperty;
}

On peut utiliser une expression conditionnelle de cette façon:

var rnd = new Random();
var a1 = new A { InnerProperty = 1 };
var a2 = new A { InnerProperty = 2 };
var result = rnd.Next() % 2 == 0 ? a1 : a2;

Suivant la valeur de la condition de l’expression conditionnelle, la 1ère ou la 2e expression est évaluée pour connaître le type du résultat. Pour que l’expression conditionnelle puisse produire un résultat prévisible, il faut que les types des résultats des expressions 1 et 2 aient des éléments communs:

  1. Les types peuvent être les mêmes: c’est le cas de l’exemple précédent. Le type du résultat est A dans cet exemple.
  2. Les types peuvent avoir un même ancêtre dans l’arbre d’héritage:
    Par exemple, si on considère les types suivants:

    public class B: A {}
    public class C: A {}
    

    On peut utiliser une expression conditionnelle de cette façon:

    var b = new B { InnerProperty = 1 };
    var c = new C { InnerProperty = 2 };
    A result = rnd.Next() % 2 == 0 ? b : c;
    

    Dans cet exemple, le type commun entre B et C est A.

  3. Une conversion implicite peut exister pour passer du type de l’expression 1 vers le type de l’expression 2 ou vice versa.
    Par exemple, si on considère les types suivants:

    public class A 
    {
      public static implicit operator A(B b) => new A();
    }
    
    public class B {}
    

    Une conversion implicite existe pour convertir des objets de type B en objets de type A.

    On peut construire ainsi une expression conditionnelle de cette façon:

    var a = new A();
    var b = new B();
    var result = rnd.Next() % 2 == 0 ? a : b;
    

    Le type du résultat sera A.

Dans le cas d’une conversion explicite:

public class A 
{
	public static explicit operator A(B b) => new A();
}

public class B {}

L’utilisation directe de l’expression conditionnelle n’est pas possible, il faut effectuer un cast explicite:

var a = new A();
var b = new B();
var result = rnd.Next() % 2 == 0 ? a : b; // ⚠ ERREUR ⚠: Type of conditional expression cannot be determined because there is no implicit conversion between ...
var result = rnd.Next() % 2 == 0 ? a : (A)b; // OK
C# 9.0

Description de la fonctionnalité

Avant C# 9.0, il fallait qu’une des 3 conditions décrites précédemment soient satisfaites pour que l’expression conditionnelle soit syntaxiquement correcte. A partir de C# 9.0, une autre condition a été rajoutée: il faut que des conversions implicites existent pour transformer le type de l’expression 1 et le type de l’expression 2 dans un type commun.

Par exemple, si on considère les types suivants:

public class A {}
public class B {}

public class C 
{
  public static implicit operator C(A a) => new C();
  public static implicit operator C(B b) => new C();
}

Des conversions implicites permettent de convertir A en C et B en C.
On peut ainsi construire une expression conditionnelle de cette façon:

var a = new A();
var b = new B();
C result = rnd.Next() % 2 == 0 ? a : b;

Cette nouvelle façon d’exécuter les expressions conditionnelles est appelée “conversion des expressions conditionnelles”.

Hiérarchie des conversions

Dans le cas de l’existence de plusieurs conversions possibles, il existe une concurrence dans les conversions et une hiérarchie est appliquée pour qu’une conversion soit appliquée plutôt qu’une autre.

Cette hiérarchie a été complexifiée par l’ajout de la conversion des expressions conditionnelles apparue en C# 9.0.
Si on considère une expression conditionnelle de cette façon:

var result = <condition> ? <expression 1> : <expression 2>;

Des conversions sont appliquées aux expressions pour obtenir le type du résultat. L’application de ces conversions se fait suivant un ordre de priorité. Ainsi une conversion est meilleure qu’une autre si:

  1. Une conversion implicite permet d’obtenir exactement le type attendu. Pour connaître toutes les conditions permettant d’indiquer qu’une expression permet d’obtenir exactement le type attendu voir Exactly matching Expression.
  2. Une expression non conditionnelle est considérée comme meilleure qu’une condition conditionnelle. En effet il est possible d’imbriquer des conditions conditionnelles:
    var result = <condition 1> ? <condition 2> ? <expression 1a> : <expression 1b> : <expression 2>;
    

    Si l’expression 2 n’est pas une expression conditionnelle, elle est considérée meilleure que: <condition 2> ? <expression 1a> : <expression 1b>

  3. Au delà de la hiérarchie des conversions d’expressions, il y a une hiérarchie dans la conversion de type. Ainsi si les expressions 1 et 2 sont toutes les deux des expressions conditionnelles ou si toutes les deux elles ne sont pas des expressions conditionnelles, une hiérarchie suivant le type cible est appliquée. Le critère le plus important est l’existence d’une conversion implicite d’un type à l’autre.
    Pour connaître la liste exhaustive des critères utilisés pour appliquer la hiérarchie des types cibles, voir Better conversion target.

Cast

Pour éviter les breaking changes, quand un cast est appliqué à une expression conditionnelle, par exemple:

T result = (T)(<condition> ? <expression 1> : <expression 2>);

Toutes les autres formes de conversion possibles mise à part la conversion de l’expression conditionnelle sont testées pour arriver au type T. La conversion de l’expression conditionnelle est utilisée en dernier ressort quand toutes les autres formes de conversions n’ont pas permis d’aboutir au type T.

Attributs sur les fonctions locales

Une évolution a été apportée pour permettre d’utiliser des attributs sur les fonctions locales (la fonctionnalité de fonction locale a été rajoutée en C# 7).

Par exemple, si on considère la fonction suivante:

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

  // Fonctions locales	
  bool isPositive(int number)
  {
    if (strictComparison)
      return number > 0;
    else
      return number >= 0;
  }
}

Si on considère l’attribut suivant:

[AttributeUsage(AttributeTargets.Method)]
public class CustomAttribute : Attribute
{}

Cet attribut est limité aux méthodes à cause de AttributeTargets.Method.
On peut placer cet attribut sur la fonction locale isPositive():

[Custom]
bool isPositive(int number)
{
  // ...
}

Paramètres ignorés dans les fonctions lambda et fonctions anonymes

Cette fonctionnalité permet de définir des expressions lambda et des fonctions anonymes en permettant d’ignorer des paramètres lorsqu’ils ne sont pas utilisés.

Pour ignorer un paramètre, il faut utiliser le caractère _ (underscore):

  • Pour les fonctions lambda, 2 syntaxes sont possibles en ignorant le nom de certains paramètres ou en ignorant le type et le nom de tous les paramètres:
    • Ignorer le nom de paramètres:
      Func<int, int, int, int> lambda = (int arg1, int _, int _) => { ... };
      
    • Ignorer le type et le nom de tous les paramètres:
      Func<int, int, int, int> lambda = (_, _, _) => { ... };
      

      Le type et le nom doivent être ignorés pour tous les paramètres, il n’est pas possible d’ignorer le type et le nom seulement pour certains paramètres:

      Func<int, int, int, int> lambda = (int arg1, _, _) => { ... }; // ⚠ ERREUR ⚠
      
  • Pour les fonctions anonymes, seulement les noms de paramètres peuvent être ignorés:
    delegate(int arg1, int _, int _) { ... }
    

    Il n’est pas possible d’ignorer le type et le nom de paramètre:

    delegate(int _, int _, int _) { ... } // OK
    delegate(_, _, _) { ... } // ⚠ ERREUR ⚠
    

Fonction lambda

Cette fonctionnalité s’utilise si une signature est imposée pour une fonction lambda ou une fonction anonyme mais que le corps de la fonction n’utilise pas tous les paramètres. Par exemple, si on considère la fonction suivante:

public int ExecuteLambda(int arg1, int arg2, int arg3, Func<int, int, int, int> lambda)
{
  return lambda(arg1, arg2, arg3);
}

On peut exécuter cette fonction en utilisant des fonctions lambda de cette façon:

Func<int, int, int, int> addIntegers = (int arg1, int arg2, int arg3) => {
  return arg1 + arg2 + arg3;
};

int result = ExecuteLambda(2, 4, 3, addIntegers);

Ou plus directement:

int result = ExecuteLambda(2, 4, 3, 
  (int arg1, int arg2, int arg3) => arg1 + arg2 + arg3);

Dans le cas où on veut utiliser ExecuteLambda() mais qu’on souhaite ignorer des arguments, la signature du paramètre lambda est imposée:

Func<int, int, int, int> identity = (int arg1, int _, int _) => {
  return arg1;
};

int result = ExecuteLambda(2, 0, 0, identity);

Avec une syntaxe plus directe:

int result = ExecuteLambda(2, 0, 0, (int arg1, int _, int _) => arg1);

On ne peut pas ignorer le type de certains arguments:

int result = ExecuteLambda(2, 0, 0, (int arg1, _,  _) => arg1); // ⚠ ERREUR ⚠

On peut ignorer le type et le nom de tous les arguments:

int result = ExecuteLambda(0, 0, 0, (_, _, _) => 0); // OK

Si _ est utilisé pour un seul caractère, il n’est pas ignoré
Dans le cas où on utilise le caractère _ pour un seul paramètre, il n’est pas ignoré. Le nom du paramètre est _. Par exemple, si on utilise _ pour un seul caractère:

Func<int, int, int, int> addIntegers = (int arg1, int _, int arg3) => {
	return arg1 + _ + arg3; // OK le paramètre n’est pas ignoré 
};

Fonction anonyme

Comme pour les fonctions lambda, certains paramètres peuvent être ignorés si la signature d’un delegate est imposée.

Si on considère le delegate suivant:

public delegate int CustomOperation(int a, int b, int c);

Et la fonction suivante utilisant le delegate

public int ExecuteDelegate(int arg1, int arg2, int arg3, CustomOperation operation)
{
  return operation(arg1, arg2, arg3);
}

On peut exécuter cette fonction en déclarant la fonction suivante au préalable:

public int AddIntegers(int arg1, int arg2, int arg3)
{
  return arg1 + arg2 + arg3;
}

L’appel s’effectue de cette façon:

int result = ExecuteDelegate(2, 4, 3, AddIntegers);

Plus directement, on peut utiliser une fonction anonyme:

int result = ExecuteDelegate(2, 4, 3, delegate(int arg1, int arg2, int arg3)
{
  return arg1 + arg2 + arg3;
});

Il est possible d’ignorer des arguments avec une fonction anonyme:

int result = ExecuteDelegate(2, 4, 3, delegate(int arg1, int _, int _)
{
  return arg1;
});

Il n’est pas possible d’utiliser une syntaxe ignorant les types des arguments:

int result = ExecuteDelegate(2, 4, 3, delegate(_, _, _)   // ⚠ ERREUR ⚠ il faut préciser le type des arguments
{
  return 0;
});

Si on utilise _ pour un seul paramètre, il n’est pas ignoré:

int result = ExecuteDelegate(2, 4, 3, delegate(int arg1, int arg2, int _)
{
  return arg1 + arg2 + _; // OK le paramètre n’est pas ignoré
});

Support de la méthode d’extension GetEnumerator() pour les boucles foreach

A partir de C# 9.0, il suffit qu’une méthode d’extension GetEnumerator() existe pour un objet donné pour qu’il soit possible d’utiliser foreach sur cet objet.

Si on souhaite effectuer une énumération sur un objet EnumerableObject, la signature de la méthode GetEnumerator() doit être:

public static IEnumerator GetEnumerator(this EnumerableObject enumerableObject)

ou

public static CustomEnumerator GetEnumerator(this EnumerableObject enumerableObject)

avec CustomEnumerator comportant les membres suivants:

  • object Current { get; }: cette propriété doit renvoyer l’objet courant dans l’objet à énumérer.
  • bool MoveNext(): cette méthode permet de passer à l’élément suivant.
  • void Reset(): permet de repositionner l’objet courant sur le 1er objet à énumérer.

Avant C# 9.0, pour pouvoir utiliser foreach sur un objet, il faut que cet objet respecte au moins une des conditions suivantes:

  • Cet objet doit satisfaire l’interface System.Collections.IEnumerable:
    Par exemple, si on considère l’objet EnumerableObject, un exemple d’implémentation pourrait être:

    public class EnumerableObject : IEnumerable
    {
      public readonly List<int> internalEnumerable;
    
      public EnumerableObject(params int[] items)
      {
        this.internalEnumerable = new List<int>(items);
      }
    
      public IEnumerator GetEnumerator()
      {
        return ((IEnumerable)this.internalEnumerable).GetEnumerator();
      }
    }
    
  • Cet objet doit satisfaire l’interface System.Collections.Generic.IEnumerable<T>:
    Par exemple, une implémentation d’un objet satisfaisant cette interface pourrait être:

    using System.Collections;
    using System.Collections.Generic;
    
    // ...
    public class EnumerableObject<T> : IEnumerable<T>
    {
      private readonly List<T> internalEnumerable;
    
      public EnumerableObject(params T[] items)
      {
        this.internalEnumerable = new List<T>(items);
      }
    
      public IEnumerator GetEnumerator()
      {
        return this.internalEnumerable.GetEnumerator();
      }
    
      IEnumerator<T> IEnumerable<T>.GetEnumerator()
      {
        return this.internalEnumerable.GetEnumerator();
      }
    }
    
  • L’objet doit comporter au moins une fonction publique dont la signature est:
    • IEnumerable GetEnumerator():
      Par exemple, une implémentation pourrait être:

      public class EnumerableObject 
      {
        public readonly List<int> internalEnumerable;
      
        public EnumerableObject(params int[] items)
        {
          this.internalEnumerable = new List<int>(items);
        }
      
        public IEnumerator GetEnumerator()
        {
          return this.internalEnumerable.GetEnumerator();
        }
      }
      
    • IEnumerable<T> GetEnumerator():
      Par exemple:

      public class EnumerableObject<T>
      {
        private readonly List<T> internalEnumerable;
      
        public EnumerableObject(params T[] items)
        {
          this.internalEnumerable = new List<T>(items);
        }
      
        public IEnumerator<T> GetEnumerator()
        {
          return this.internalEnumerable.GetEnumerator();
        }
      }
      
    • CustomEnumerator GetEnumerator():
      Par exemple, une implémentation pourrait être:

      public class EnumerableObject
      {
        public readonly List<int> internalEnumerable;
        public readonly CustomEnumerator enumerator;
      
        public EnumerableObject(params int[] items)
        {
          this.internalEnumerable = new List<int>(items);
          this.enumerator = new CustomEnumerator(this.internalEnumerable.GetEnumerator());
        }
      
        public CustomEnumerator GetEnumerator()
        {
          return this.enumerator;
        }
      }
      

      CustomEnumerator doit comporter les membres Current, MoveNext() et Reset(), par exemple:

      public class CustomEnumerator
      {
        private readonly IEnumerator enumerator;
      
        public CustomEnumerator(IEnumerator enumerator)
        {
          this.enumerator = enumerator;
        }
      
        public object Current => this.enumerator.Current;
      
        public bool MoveNext()
        {
          return this.enumerator.MoveNext();
        }
            
        public void Reset()
        {
          this.enumerator.Reset();
        }
      }
      
C# 9.0

Depuis C# 9.0, pour énumérer un objet avec foreach, il suffit qu’il existe au moins une méthode d’extension avec la signature suivante:

  • public static IEnumerator GetEnumerator(this EnumerableObject enumerableObject):
    Par exemple:

    public static class EnumeratorHelper
    {
      public static IEnumerator GetEnumerator(this EnumerableObject enumerableObject)
      {
          return enumerableObject.internalEnumerable.GetEnumerator();
      }
    }
    
  • public static CustomEnumerator GetEnumerator(this EnumerableObject enumerableObject):
    Par exemple:

    public static class EnumeratorHelper
    {
      public static CustomEnumerator GetEnumerator(this EnumerableObject enumerableObject)
      {
          return new CustomEnumerator(enumerableObject.internalEnumerable);
      }
    }
    

L’implémentation de CustomEnumerator est similaire à celle plus haut.

Autres fonctionnalités

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

Références

Leave a Reply