Fonctionnalités C# 7

La version 7 de C# a été très riche en fonctionnalités, le but de cet article est de les résumer et de les expliquer. Dans un premier temps, on explicitera le contexte de C# 7 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#

En parallèle de la sortie des différentes versions de C# 7, l’environnement .NET s’est étauffé avec .NET Core. La possibilité de pouvoir compiler du code C# à partir de plusieurs frameworks et la sortie du compilateur Roslyn ont entraîné plusieurs modifications dans la façon dont l’environnement de développement est installé. Le but de cette partie est d’expliciter quelques-unes de ces modifications.

Chronologie des releases

Ce tableau permet de résumer les dates de sorties des versions de C# 7, de Visual Studio, du compilateur historique implémenté en C++, 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
Juillet 2015 C# 6.0 VS 2015 (14.0) 6.0
Roslyn 1.0
.NET 4.6
(.NET Standard 1.0⇒1.3)
N/A
Novembre 2015 .NET 4.6.1
(.NET Standard 1.0⇒2.0)
Juin 2016 6.0
Roslyn 1.3
Aout 2016 .NET 4.6.2
(.NET Standard 1.0⇒2.0)
.NET Core 1.0
(.NET Standard 1.0⇒1.6)
Novembre 2016 .NET Core 1.1
(.NET Standard 1.0⇒1.6)
Mars 2017 C# 7.0 VS 2017 (15.0) 7.0
Roslyn 2.0
Avril 2017 VS 2017 (15.1) 7.0
Roslyn 2.1
.NET 4.7
(.NET Standard 1.0⇒2.0)
Mai 2017 VS 2017 (15.2) 7.0
Roslyn 2.2
Aout 2017 C# 7.1 VS 2017 (15.3) 7.1
Roslyn 2.2
Septembre 2017 7.1
Roslyn 2.3
.NET Core 2.0
(.NET Standard 1.0⇒2.0)
Octobre 2017 VS 2017 (15.4) 7.1
Roslyn 2.4
.NET 4.7.1
(.NET Standard 1.0⇒2.0)
Décembre 2017 C# 7.2 VS 2017 (15.5) Roslyn 2.5
Mars 2018 VS 2017 (15.6) Roslyn 2.6
Avril 2018 .NET 4.7.2
(NET Standard 1.0⇒2.0)
Mai 2018 C# 7.3 VS 2017 (15.7) Roslyn 2.7/2.8 .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 VS 2019 (16.3) .NET Core 3.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. Cependant il faut garder en tête que la version de C# est liée, en premier lieu, à la version du compilateur C# csc qui va générer le code exécutable pour un runtime donné. Ensuite la version du compilateur peut être liée au framework installé, à Visual Studio ou au SDK .NET Core.

La livraison du compilateur C# a changé suivant les versions des frameworks toutefois pour toutes les versions du compilateur, il suffit de taper la commande suivante pour que la version soit indiquée:

csc.exe -help

Par exemple:

C:\Program Files\Microsoft Visual Studio\2017\Community>csc -help
Compilateur Microsoft (R) Visual C# version 2.10.0.0 (b9fb1610)
Copyright (C) Microsoft Corporation. Tous droits réservés.

Le chemin de csc.exe change suivant la version de Visual Studio ou celle du framework .NET.

Avant Visual Studio 2017

Avant Visual Studio 2017, le compilateur C# était livré avec le framework .NET. Le framework .NET a lui-même été livré de façon différente avant et après .NET 4.0:

  • Avant le framework .NET 4.0: chaque framework faisait l’objet d’une installation séparée et chaque nouvelle version était rajoutée aux versions existantes sur une machine donnée. Ainsi les chemins des compilateurs dans les différentes versions de framework étaient:
    • .NET v2.0: C:/Windows/Microsoft.NET/Framework/v2.0.50727/csc.exe
    • .NET v3.5: C:/Windows/Microsoft.NET/Framework/v3.5/csc.exe
    • .NET v4.0: C:/Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe
  • A partir du framework 4.5: pour chaque nouvelle installation, les fichiers du frameworks sont remplacés par ceux de la nouvelle version. L’ancien framework est donc remplacé par le nouveau, c’est la version des assemblies du framework (par exemple clr.dll) qui peuvent indiquée quelle est la version du framework installée. Le chemin du compilateur est:
    • Avant Visual Studio 2013: le chemin est le même qu’auparavant: C:/Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe.
    • Après Visual Studio 2013:
      • Sur un système d’exploitation 32-bit: C:/Program Files/MSBuild/<version de MsBuild>/Bin/csc.exe.
      • Sur un système d’exploitation 64-bit: C:/Program Files (x86)/MSBuild/<version MsBuild>/Bin/csc.exe

Pour les compilateurs C# livrés avec des frameworks avant Visual Studio 2017, en tapant csc -help, on peut voir la version C# qui est gérée:

C:\Windows\Microsoft.NET\Framework\v4.0.30319>csc.exe -help
Microsoft (R) Visual C# Compiler version 4.7.3062.0 for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only
supports language versions up to C# 5, which is no longer the latest version. Fo
r compilers that support newer versions of the C# programming language, see http
://go.microsoft.com/fwlink/?LinkID=533240

Pour toutes les versions du compilateur avant Visual Studio 2017, la version évolue suivant les frameworks installés:

  • 1.0 pour le framework .NET 1.0
  • 2.0 pour le framework .NET 2.0 (Visual Studio 2005)
  • 3.5 pour le framework .NET 3.5 (Visual Studio 2008)
  • 4.0 pour le framework .NET 4.0
  • 4.x pour les frameworks .NET de 4.5 à 4.7.1.

Comme indiqué précédemment, toutes ces versions du framework impliquent que plusieurs versions du compilateur peuvent cohabiter sur la même machine toutefois si on utilise la ligne de commandes Développeur (i.e. Developer Command Prompt) c’est la dernière version qui sera utilisée. Pour savoir le chemin du compilateur disponible à la ligne de commandes, il faut exécuter la commande:

where csc 

A partir de Visual Studio 2017

Visual Studio 2017 a marqué un changement notable pour le compilateur C# puisque l’ancien compilateur implémenté en C++ a été remplacé par Roslyn qui est open source et implémenté en C#. Ce compilateur n’est pas apparu dans les premières versions de Visual Studio 2017 mais à partir de la version 15.3 (août 2017). Les versions précédentes de Visual Studio 2017 (15, 15.1 et 15.2) utilisaient l’ancien compilateur.

Avec Roslyn, le compilateur n’est plus livré avec le framework mais avec Visual Studio, avec les Build tools ou avec le SDK .NET Core. Le chemin du compilateur n’est plus lié au framework comme auparavant:

  • Avec Visual Studio: par exemple pour Visual Studio 2017 Professional: C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\csc.exe
  • Avec les Build tools: par exemple pour les Build Tools for Visual Studio 2017: C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\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

Avec le nouveau compilateur Roslyn, la version du compilateur est reparti à 1 par rapport au compilateur précédent:

  • 1.x pour les 1ère versions livrées avec Visual Studio 2015 supportant C# 6.0 et versions précédentes.
  • 2.x pour les versions livrées avec Visual Studio 2017 et supportant C# 7.x.

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>7.1</LangVersion> 
        </PropertyGroup> 
    </Project> 
    

Fonctionnalités C# 7.x

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

Fonctions locales

C# 7.0

On peut déclarer des fonctions à l’intérieur d’autres fonctions.

Avant C# 7.0, on pouvait utiliser des expressions lambda, par exemple:

IEnumerable<int> GetPositiveNumber(IEnumerable<int> numbers, bool strictComparison) 
{ 
  var isPositive => n => { 
    if (strictComparison) 
      return n > 0; 
    else 
      return n >= 0; 
    } 

  return numbers.Where(n => isPositive(n)); 
} 

A partir de C# 7.0, on peut déclarer une fonction directement dans le corps d’une autre fonction:

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

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

La fonction locale fait partie du contexte de sa fonction parente, il est donc possible d’accéder dans la fonction locale aux arguments et variables locales de la fonction parente.

Arguments out d’une méthode

C# 7.0

Avant C# 7.0, pour utiliser le mot-clé out dans les arguments d’une méthode, il fallait déclarer le paramètre avant l’appel à la méthode:

public static bool TryFindFirstPositiveNumber(IEnumerable<int> numbers, out firstPositiveNumber) 
{ 
  var positiveNumbers = numbers.Where(n => n > 0); 
  if (positiveNumbers.Any()) 
  { 
    firstPositiveNumber = positiveNumbers.First(); 
    return true; 
  } 
  else 
  { 
    firstPositiveNumber = 0; 
    return false; 
  } 
} 

Avant C# 7.0, l’appel est du type:

int[] numbers = {-1, -2, 0, 5, 8}; 
int firstPositiveNumber; 
 
if (TryFindFirstPositiveNumber(numbers, out firstPositiveNumber)) 
{ 
  Console.WriteLine(firstPositiveNumber); 
} 

A partir de C# 7.0, on peut déclarer la variable directement lors de l’appel avec out:

if (TryFindFirstPositiveNumber(numbers, out int firstPositiveNumber)) 
{ 
  Console.WriteLine(firstPositiveNumber); 
} 

Dans le cas de cette fonction, le scope de la variable firstPositiveNumber déclarée avec out est le même que si la variable est déclarée en dehors de l’appel.

Eviter les déclarations de variables inutiles

C# 7.0

Certaines syntaxes imposent de devoir définir des variables même si on ne souhaite pas s’en servir par la suite, par exemple si on utilise un méthode avec un argument out et qu’on ne souhaite pas utiliser cet argument:

string valueAsString = "6" 
if (int.TryParse(valueAsString, out int valueAsInt)) 
  Console.WriteLine("Value is an integer."); 

Dans cet exemple valueAsInt ne sert pas, toutefois on est obligé de le déclarer à cause de la signature de la fonction. A partir de C# 7.0, il est possible d’ignorer l’argument en utilisant le caractère _ de façon à alléger la syntaxe:

if (int.TryParse(valueAsString, out _)) 
  Console.WriteLine("Value is an integer."); 

D’autres cas de figure permettent d’ignorer une variable:

  • Lors de la déconstruction d’un tuple:
    var tuple = (6, "6", 6.0f); 
    var (ValueAsInt, ValueAsString, ValueAsFloat) = tuple; // Déconstruction du tuple 
    Console.WriteLine($"Int value is {ValueAsInt}."); // ValueAsString et ValueAsFloat sont inutiles 
    

    On peut ignorer de déclarer des variables inutilement lors de la déconstruction:

    var (ValueAsInt, _, _) = tuple; // Déconstruction 
    Console.WriteLine($"Int value is {ValueAsInt}."); 
    
  • Pour une variable locale (possible mais pas très utile):

    Par exemple si on considère le code suivant:

    private static async Task UselessTask() 
    { 
      await Task.Delay(10000); 
      Console.WriteLine("Completed"); 
    } 
    
    static void Main() 
    { 
      UselessTask(); // WARNING: CS4014
      Console.ReadLine(); 
    } 
    

    Ce code n’attends pas la fin de l’exécution de la task asynchrone exécutée dans UselessTask(). A la compilation, il entraîne le message de Warning:

    Warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the ‘await’ operator to the result of the call. 
    

    Si on souhaite éviter le message de warning et ne pas attendre l’exécution de la tâche, on peut ignorer le retour de la fonction:

    static void Main() 
    { 
      _ = UselessTask(); // Pas de warning 
      Console.ReadLine(); 
    } 
    

    Il est possible d’utiliser _ en tant que nom de variable. Toutefois si _ est utilisé pour désigner une variable, il ne sera plus possible d’utiliser ce caractère pour indiquer une variable:

    var _ = 5; // OK
    
    _ = UselessTask(); // ERROR: cette ligne est considéré comme étant une affectation. 
    // _ est de type int, l'affectation n'est pas possible.
    

  • Avec is:
    L’opérateur is permet de tester si une expression correspond à un type, par exemple si on considère la classe suivante:

    class Square 
    { 
      public int Size; 
    } 
    

    is permet de tester si l’objet squareAsObj est de type Square:

    object squareAsObj = new Square { Size = 6 }; 
    if (squareAsObj is Square) // permet de tester le type de squareAsObj 
    { 
      Square typedSquare = squareAsObj as Square; 
      Console.WriteLine($"{nameof(squareAsObj)} is a square of size {typedSquare.Size}."); 
    } 
    

    A partir de C# 7.0, il est possible de simplifier l’utilisation de is et as en une seule ligne:

    if (squareAsObj is Square typedSquare) // permet de tester le type et d'initialiser la variable typée typedSquare 
      Console.WriteLine($"{nameof(squareAsObj)} is a square of size {typedSquare.Size}."); 
    

    Par suite, on peut ignorer la création d’une variable avec _:

    if (squareAsObj is Square _) // La création de la variable est ignorée 
      Console.WriteLine($"{nameof(squareAsObj)} is a square."); 
    

    L’exemple précédent complexifie inutilement la syntaxe puisqu’il suffirait d’écrire if (squareAsObj is Square) pour obtenir le même résultat. L’intérêt de cette syntaxe est de simplifier l’écriture avec switch (point suivant).

  • Avec switch...case:
    A partir de C# 7.0, il est possible d’utiliser switch...case pour tester le type d’une variable, par exemple (la classe Square est déclarée plus haut):

    object squareAsObj = new Square { Size = 6 }; 
    switch (squareAsObj) 
    { 
      case Square typedSquare: // permet de tester le type et d'initialiser la variable typée typedSquare 
        Console.WriteLine($"{nameof(squareAsObj)} is a square of size {typedSquare.Size}."); 
        break; 
      default: 
        Console.WriteLine($"{nameof(squareAsObj)} is not a square."); 
        break; 
    } 
    

    case Square typedSquare est équivalent à if (squareAsObj is Square typedSquare). De la même façon que is, il est possible d’ignorer la création d’une variable en utilisant _:

    switch (squareAsObj) 
    { 
      case Square _: // permet de seulement tester le type 
        Console.WriteLine($"{nameof(squareAsObj)} is a square."); 
        break; 
      default: 
        Console.WriteLine($"{nameof(squareAsObj)} is not a square."); 
        break; 
    } 
    

Support de async dans le Main

C# 7.1

Avant C# 7.1, quand la méthode Main() d’une application appelait une méthode async, on devait utiliser une syntaxe similaire à celle-ci:

class Program 
{ 
  static void Main(string[] args) 
  { 
    UselessTask().GetAwaiter().GetResult(); 
    // ou 
    UselessTask().Wait(); 
    // ou 
    int result = UselessTaskWithResult().GetAwaiter().GetResult(); 
    // ou 
    int result = UselessTaskWithResult().Result; 
  } 
 
  private static async Task UselessTask() 
  { 
    await Task.Delay(10000); 
    Console.WriteLine("Completed"); 
  } 

  private static async Task<int> UselessTaskWithResult() 
  { 
    await Task.Delay(10000); 
    Console.WriteLine("Completed"); 
    return 0; 
  } 
} 

A partir de C# 7.1, la fonction Main() supporte la notation async/await, il est possible d’utiliser une syntaxe plus simple:

static async Task Main(string[] args) 
{ 
  await UselessTask(); 
} 

ou

static async Task<int> Main(string[] args) 
{ 
  return await UselessTaskWithResult(); 
} 

D’autres syntaxes pour définir la fonction Main() sont possibles:

Avant C# 7.0
static void Main(string[] args)
static int Main(string[] args)
static void Main()
static int Main()
A partir de C# 7.1 En plus de surcharges précédentes, on peut utiliser:

static async Task Main(string[] args)
static Task Main(string[] args)
static async Task<T> Main(string[] args)
static Task<T> Main(string[] args)
static async Task Main()
static Task Main()
static async Task<T> Main()
static Task<T> Main()

Mot-clé default

C# 7.1

A partir de C# 7.1, on peut simplifier la syntaxe default(T) par default. default et default(T) permettent au compilateur de produire une valeur par défaut suivant le type voulu obtenu par déduction:

  • Pour les types références, la valeur par défaut sera null.
  • Les chaînes de caractères bien qu’étant des objets de type référence, ont pour valeur par défaut une chaîne vide (i.e. string.Empty);
  • Les types valeurs usuels ont pour valeur par défaut la valeur 0 correspondant au type exact: 0 pour int, uint, long ou ulong; 0f pour float; 0m pour decimal; 0d pour double; false pour bool etc…

Par exemple:

int intValue = default; // 0 même comportement que default(int).
float floatValue = default; // 0f ATTENTION à la comparaison entre un flottant et 0
string strValue = default; // Chaîne vide (string.Empty)

Dans le cas d’un objet de type référence, la valeur par défaut est null:

class EmptyClass {}

EmptyClass instance = default; // null

Dans le cas d’une structure, la valeur par défaut n’est pas null mais une instance dans laquelle les membres ont une valeur par défaut:

struct SimpleStruct
{
  public int InnerMember;
}

// ...
SimpleStruct instance = default; // N’est pas égal à null
Console.WriteLine(instance.InnerMember); // 0

default ou default(T) peuvent être utilisés en dehors de l’initialisation comme par exemple dans une comparaison:

Console.WriteLine(default == 0); // true. C’est le compilateur qui déduit la valeur de default en fonction de 0

Un des intérêts de default ou default(T) est de pouvoir utiliser une valeur par défaut quand l’utilisation de type générique ne permet pas de déterminer le type exact, par exemple:

T GetValue<T>()
{
  return default; // Le type réel de T n’est pas connu
}

Opérateur de portée private protected

C# 7.2

A partir de C# 7.2, l’opérateur de portée private protected a été rajouté, si un membre est décoré de cet opérateur dans une classe, il ne sera accessible que par les membres et les classes dérivant de cette classe se trouvant dans la même assembly.

Pour résumer:

  • protected internal: l’accès est limité aux objets de l’assembly courante ou aux classes dérivant de la classe où se trouve l’opérateur (les classes dérivées peuvent se trouver dans une autre assembly).
  • private protected: l’accès est limité aux classes dérivant de la classe où se trouve l’opérateur. Les classes dérivées doivent se trouver obligatoirement dans la même assembly.

Utilisations plus larges des expressions

Une application C# est constituée d’instructions (i.e. statements) faites de mot-clés, d’expressions et d’opérateurs. Une expression correspond à une instruction permettant d’obtenir une valeur comme par exemple une constante, une variable, le résultat d’une fonction ou une suite d’opération avec des opérandes. Le résultat d’une expression peut être affecté à un variable, servir d’argument à une méthode ou à une autre opération.
A l’opposé, les instructions qui ne sont pas des expressions peuvent être des déclarations ou des assignations de variables etc…

Dans les 1ères versions de C#, les expressions étaient réservées aux corps des méthodes. A partir de C# 6, la syntaxe a permis d’utiliser des expressions dont le corps peut définir:

  • des expressions lambda,
  • directement des méthodes ou
  • des propriétés en lecture seule avec une notation sans accolades (i.e. expression-bodied).

Cette syntaxe est du type:

<élément> => <expression>

L’élément pouvant être une expression lambda, une méthode à déclarer dans une classe ou une propriété en lecture seule.

Par exemple:

// Pour déclarer une méthode
private int innerVariable = 5;
public void DisplayInnerVariable() => Console.WriteLine(this.innerVariable); // les accolades sont omises

// Pour déclarer une fonction
public int AddToInnerVariable(int add) => this.innerVariable + add; // return est omis

// Pour déclarer une propriété en lecture
public int InnerVariable => this.innerVariable; // return est omis

Syntaxe réduite du corps d’une expression

C# 7.0

C# 7 a permis d’étendre l’utilisation des expressions à d’autres types d’instructions comme le constructeur, les propriétés en écriture ou les exceptions lancées avec throw.

On peut utiliser des expressions avec une syntaxe réduite directement pour:

  • Propriété en écriture:
    La syntaxe permet d’utiliser des expressions directement dans des propriétés, par exemple:

    public int InnerVariable
    {
      get => this.innerVariable; // En lecture, pas d’accolade et return est omis
      set => this.innerVariable = value; // En écriture, la valeur à paramétrer est dans value
    }
    
  • Constructeur:
    public class Circle
    {
      private int Radius;
    
      public Circle(int radius) => this.Radius = radius; // Les accolades sont omises
    }
    
  • Destructeur:
    Même syntaxe que pour le constructeur:

    public class Circle
    {
      private int Radius;
    
      ~Circle() => this.Radius = 0;
    }
    
  • Index:
    Les surcharges d’index sur un objet peuvent aussi utiliser une syntaxe sous la forme d’expressions en lecture/écriture:

    public class Séquence
    {
      private readonly List<int> sequence = new List<int> {
        0, 1, 1, 2, 3, 5, 8, 13, 21, 34
      }
    
      public int this[int i]
      {
        get => this.sequence[i];
        set => this.sequence[i] = value;
      }
    }
    

throw <exception> est désormais une expression

C# 7.0

Auparavant throw <exception> n’était pas considéré comme une expression (c’est-à-dire qu’on ne peut pas extraire de l’instruction throw <exception> une valeur assignable). Il n’était donc possible d’utiliser throw <exception> que dans un bloc de code:

int uselessValue = -1;
if (uselessValue < 0)
  throw new InvalidOperationException(); // Dans un bloc de code particulier

throw <exception> ne pouvait pas être utilisé en tant qu’expression, par exemple dans une expression ternaire:

// Avant C# 7.0, cette syntaxe n’est pas possible
  int newValue = uselessValue > 0 ? uselessValue : throw new InvalidOperationException();

A partir de C# 7.0, le compilateur traite throw <exception> comme une expression de façon à ne pas avoir d’erreur de syntaxe à la compilation quand on l’utilise en tant qu’expression.

Par exemple:

  • Quand on définit une expression lambda:
    Func<float, int> convertToInt = (number) => throw new NotImplementedException();
    
  • Dans un constructeur:
    public class Circle
    {
      private int Radius;
    
      public Circle(int radius) => throw new NotImplementedException();
    }
    

    Cette syntaxe est possible pour tous les exemples d’utilisation de syntaxe réduite du corps d’une expression présentés plus haut.

  • Si on utilise une expression ternaire:
    bool intValue = 1;
    string exitCode = intValue > 0 ? "OK" : throw new InvalidOperationException();
    
  • etc…

Déclaration de variables

C# 7.3

A partir de C# 7.3, il est possible d’implémenter la déclaration de variables avec out var <variable> directement dans la version réduite du corps d’une expression.

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

public static bool CanExtractIntIfOdd(string inputStr, out int extractedNumber)
{
  return int.TryParse(inputStr, out extractedNumber)
    && extractedNumber % 2 != 0;
}

Il est possible d’utiliser la construction out var <variable> dans une des formes réduites utilisées plus haut, par exemple dans le constructeur:

public class Test
{
  private readonly bool isNumberOddAndPositive;

  public Test(string intAsString) => this.isNumberOffAndPositive = CanExtractIntIfOdd(intAsString, out int extractedNumber)
    && extractedNumber > 0;
}

Autres fonctionnalités

Les fonctionnalités suivantes ne sont pas présentées dans cet article:

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

Références

Leave a Reply