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#
Chronologie des releases
Lien entre la version C# et le compilateur
Limiter la version C# à compiler
Fonctionnalités C# 8.0
Fonction locale statique
Utilisation de using sans bloc de code
Méthode d’interface par défaut
Accessible avec le type de l’interface
Modifier l’implémentation dans la classe
Accéder à la méthode dans la classe
Implémenter une méthode statique dans l’interface
Index et plage d’une liste
System.Index
System.Range
Amélioration des chaines de caractères textuelles interpolées
Autres fonctionnalité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
- Sur Linux:
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ètreLangVersion
:<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:
- Références nullables (i.e. nullable reference types),
- Opérateur ??= (i.e. null-coalescing assignment),
- Pattern matching,
- Enumération asynchrone (i.e. asynchronous streams),
- Disposer des objets de façon asynchrone (i.e. asynchronous disposable),
- Unmanaged constructed types,
- Membre d’une structure en lecture seule avec
readonly
(i.e. readonly members), ref struct
etreadonly ref struct
disposable (i.e. disposable ref structs),- Utilisation de
stackalloc
dans une expression (i.e. stackalloc in nested expressions)
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:
- Fonction locale statique,
- Utilisation de using sans bloc de code (i.e. using declarations),
- Amélioration des chaines de caractères textuelles interpolées (i.e. interpolated verbatim strings),
- Références nullables (i.e. nullable reference types): les attributs
AllowNull
,DisallowNull
,MaybeNull
,NotNull
,NotNullWhen
,MaybeNullWhen
etNotNullIfNotNull
ne sont, toutefois, pas disponibles directement. - Opérateur ??= (i.e. null-coalescing assignment),
- Pattern matching,
- Membre d’une structure en lecture seule avec
readonly
(i.e. readonly members), ref struct
etreadonly ref struct
disposable (i.e. disposable ref structs),- Unmanaged constructed types.
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:
- En utilisant le package NuGet (non officiel) Microsoft.Bcl.AsyncInterfaces pour les fonctionnalités:
- En utilisant le package System.Memory pour obtenir le type
Span<T>
pour la fonctionnalité “Utilisation destackalloc
dans une expression” (i.e. stackalloc in nested expressions). - En implémentant directement les types pour la fonctionnalité “Index et plage d’une liste” (i.e. indices and range).
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
ouoverride
.
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:
System.Index
représentant l’index d’une liste etSystem.Range
étant une plage d’index d’une liste.
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 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 contenantC:\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\"
). - Avec le caractère
- 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:
- 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 '.
- Si le nombre de caractères d’alignement > 0 ⇒ des espaces sont rajoutés à gauche, par exemple:
- 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
etsyntax2
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}}
. - Permettre d’aligner des chaînes en indiquant un nombre minimum de caractères avec la syntaxe
Autres fonctionnalités
Les autres fonctionnalités sont traitées dans d’autres articles:
- Références nullables (i.e. nullable reference types),
- Opérateur ??= (i.e. null-coalescing assignment),
- Pattern matching,
- Enumération asynchrone (i.e. asynchronous streams),
- Disposer des objets de façon asynchrone (i.e. asynchronous disposable),
- Unmanaged constructed types,
- Membre d’une structure en lecture seule avec
readonly
(i.e. readonly members), ref struct
etreadonly ref struct
disposable (i.e. disposable ref structs),- Utilisation de
stackalloc
dans une expression (i.e. stackalloc in nested expressions)
- What’s new in C# 8.0: https://docs.microsoft.com/fr-fr/dotnet/csharp/whats-new/csharp-8
- Enhancement of interpolated verbatim strings: https://docs.microsoft.com/fr-fr/dotnet/csharp/whats-new/csharp-8#enhancement-of-interpolated-verbatim-strings
- Does C# 8 support the .NET Framework?: https://stackoverflow.com/questions/56651472/does-c-sharp-8-support-the-net-framework
- Update libraries to use nullable reference types and communicate nullable rules to callers: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies
- Microsoft.Bcl.AsyncInterfaces: https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces/
- C# 8.0 and .NET Standard 2.0 – Doing Unsupported Things: https://stu.dev/csharp8-doing-unsupported-things/
- How to use Nullable Reference Types in .NET Standard 2.0 and .NET Framework: https://www.meziantou.net/how-to-use-nullable-reference-types-in-dotnet-standard-2-0-and-dotnet-.htm
- Consider using C# 8 with the .NET Framework: https://medium.com/@joni2nja/consider-using-c-8-with-the-net-framework-9dceb20647c5
- Using async disposable and async enumerable in frameworks older than .NET Core 3.0: https://www.strathweb.com/2019/11/using-async-disposable-and-async-enumerable-in-frameworks-older-than-net-core-3-0/
- Index Structure: https://docs.microsoft.com/fr-fr/dotnet/api/system.index
- Range Structure: https://docs.microsoft.com/fr-fr/dotnet/api/system.range
- What’s the @ in front of a string in C#?: https://stackoverflow.com/questions/556133/whats-the-in-front-of-a-string-in-c
- $ – string interpolation: https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/tokens/interpolated
- Strings in C# and .NET: https://csharpindepth.com/Articles/Strings
- How can I use C# 8 with Visual Studio 2017?: https://stackoverflow.com/questions/54701377/how-can-i-use-c-sharp-8-with-visual-studio-2017/58190585#58190585
- https://www.nuget.org/packages/Microsoft.Net.Compilers.Toolset/: https://www.nuget.org/packages/Microsoft.Net.Compilers.Toolset
- Microsoft.Net.Compilers: https://www.nuget.org/packages/Microsoft.Net.Compilers/
- Microsoft annonce la fin de .NET Standard, sa couche de base unique pour toutes les applications .NET, y compris Xamarin: https://dotnet.developpez.com/actu/308950/Microsoft-annonce-la-fin-de-NET-Standard-sa-couche-de-base-unique-pour-toutes-les-applications-NET-y-compris-Xamarin-il-sera-remplace-par-NET-5/
- dotnet / roslyn : https://github.com/dotnet/roslyn