Introduction à .Net Core 3
.NET Core 3 est la prochaine nouvelle version de .NET Core, le Framework open source de Microsoft qui va prendre en charge les différentes fonctionnalités suivantes :
– prise en charge de WinForm
– prise en charge de WPF
– EntityFramework 6
– création d’applications web coté client avec Razor
– C# 8
– Internet des objets IOT (Internet-Of-Things)
– le support ML.NET (machine learning)
C# 8
C# 8 est la nouvelle version majeur du langage C# que Microsoft prévois de livrer en même temps que .Net Core3 et Visual studio 2019.
Cette version fournira plusieurs fonctionnalités, et parmi eux des fonctionnalités principales tel que :
- Types de référence nullables
- Les types Range et Index
- Les Flux asynchrones
- Implémentation par défaut des membres d’interfaces
- …
Bien évidemment je reviendrai juste après en détail sur chacune d’elle.
Installation et configuration de C# 8
On peut télécharger la version « Preview » de .Net Core 3, et travailler avec Visual Studio Code, ou bien avec la version « Preview de Visual studio 2019 »
- les étapes d’installation et configuration avec VS CODE :
- Installer VSCODE sur le lien : https://code.visualstudio.com/
- Installer la version « Preview » de .NET Core SDK qui se trouve sur : https://dotnet.microsoft.com/download/dotnet-core/3.0
- installer l’extension C# de Visual studio Code (OmniSharp ), depuis le menu « Install From VSIX » de vs Code, ou bien directement via VS CODE extensions
À la fin l’installation vous pouvez vérifier si elle est bien faite en utilisant la commande suivante sous Ms-Dos ou PowerShell
dotnet –version :
Quand vous créez votre premier projet en C#8, les fonctionnalités de cette version ne sont pas activée par défaut, pour les activer il faut modifier le fichier projet de votre applications c’est à dire le « .csproj » pour ajouter les deux lignes suivantes :
- <LangVersion>8.0</LangVersion> : pour l’activation de C# 8
- <NullableContextOptions>enable</NullableContextOptions> : cette ligne permet d’activer l’une des fonctionnalités far de cette nouvelle version de C# qui est les références nullables.
« NullableContextOptions » une fois activée, si vous avez des variables références dans votre code elles deviendront non nullable, cette option va nous servir l’ors des migrations de l’ancien code vers le nouveau, j’en parlerai un peu plus tard.
Et de même si vous créez des variables de type référence null , exemple string? Chaine, et si la fonctionnalité est désactivé alors vous aurez un Warning de ce genre : The annotation for nullable reference types should only be used in code within a ‘#nullable’ context.
- Les étapes d’installation et configuration avec VS 2019
- installez la version « preview » de Visual studio 2019 qui est sur le lien suivant : https://visualstudio.microsoft.com/vs/preview/
- Installer la version .Net Core 3 comme dit auparavant
- lancer l’installation de Visual studio 2019, vous pouvez prendre la version community
- Activer le .Net Core 3 dans Tools->Options->Projects and Solutions->.Net Core :
et cochez la case : use previews of the .NET Core SDK et redémarrez Visual studio
- maintenant il faut activer le C# 8 dans les propriétés de projet dans l’onglet build cliquez sur le bouton « advanced Build Settings »: il faut choisir C#8.0 (beta)
Et voilà le tour et joué, vous pouvez maintenant commencer à travailler avec le nouveau C#8.
Types référence Nullable
Qui se souvient bien de la fameuse exception : « la référence d’objet n’est pas définie à une instance d’un objet », je pense que chacun de nous a eu ce genre d’erreur durant son expérience, c’est souvent difficile à trouver la cause de cette exception, mais généralement elle est relié aux types références, et le fait d’essayer d’accéder à un membre d’un objet qui est Null, le système lève cette exception : « system.nullreferenceexception ».
Dans cette version de C# 8 Microsoft veut aider les concepteurs et développeurs à diminuer de cette exception sous réserve bien sûr que les concepteurs feront un effort supplémentaire pour distinguer d’une manière explicite et responsable des objets qui pourront être null ou pas, et aux développeurs d’implémenter correctement cette conception en faisant en sorte que les types référence n’accepterons plus de valeurs null, sauf si on les déclare nullables explicitement avec la même syntaxe que les types nullable :
Exemple : int? valeur = null : déclaration d’un type int nullable, désormais pour déclarer un string nullable on fait pareil : string? chaine = null.
Et par conséquent ça ne sera plus possible de déclarer comme ceci : string chaine = null; si on le fait on aura un Warning suivant :
Pour l’instant Microsoft prévoit un moyen pour pouvoir activer ou pas cette fonctionnalité dans le but de ne pas impacter le code existant, mais pour l’instant même l’impact n’est pas vraiment grand car, au lieu d’afficher des erreurs le compilateur affichera des Warnings (avertissements), cette nouvelle fonctionnalité reconnaîtra les différentes méthodes existantes de vérification de null, et donc pas de panique car y a pas d’obligation de modification , par contre prévoir une migration du code c’est mieux.
Pour comprendre un peu la philosophie de Microsoft sur le choix de nullable pour les types références regardant cet exemple définit dans le blog dev de microsoft:
class Person
{
public string FirstName; // non null
public string? MiddleName; // peut etre null
public string LastName; // non null
}
Logiquement il est très proche de la réalité qu’une personne doit obligatoirement avoir un nom, un prénom, et ne pas avoir forcément un deuxième prénom « Middlename », par conséquent le développeur doit déclarer MiddleName comme étant Nullable.
Ranges et indexes
Indexes : Les index sont utilisés pour l’indexation d’un tableau, il y a deux sortes d’index un index qui commence de début de tableau avec la position 0, et l’index qui commence par la fin de tableau avec la position 1
c’est à dire : admettant on a un tableau d’entier : int[] Tab = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
On déclare un index comme suite:
Index ibegin = 4; // index qui commence le comptage de début
Index iend = ^6; // index qui commence le comptage de la fin de tableau en commençant par 1, le signe ^ positionne l’index à partir de la fin du tableau
Tab[ibegin] affichera 4, le 4eme élément de tableau en commençant le comptage de 0 de début de tableau.
Tab[iend] affichera 5, le 6eme élément de tableau en commençant le comptage de 1 de la fin de tableau.
Range : un range c’est un nouveau type structure qui contient un index début et un index fin on déclare un range comme suite :
char[] charArray = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' };
Range r = new Range(3, ^2);
Range r1 = 3..^2;
les deux instructions affichent « DEF » , on commence à partir de la position 3 de début de tableau qui est D et à partir de la fin du tableau on compte 2 position donc le F.
Autres exemples:
charArray[3..3] de la position 3 de à partir de début jusqu’à la position 3 à partir de début , renvoi un tableau vide
charArray[^3,^3] de la position 3 à partir de la fin jusqu’à la position à 3 à partir de la fin, renvoi un tableau vide.
charArray[4..^8] de la position 4 de début jusqu’à la position 8 à partir de la fin, renvoi une exception
charArray[..8] de début de tableau jusqu’à la position 8 à partir de la fin, renvoi tout le tableau
charArray[4..] de la position 4 à partir de début de tableau jusqu’à la fin du tableau, renvoi EFGH
Attention : quand vous donnez des indexes début et fin qui se chevauchent vous aurez une exception de type : System.OverflowException : Arithmetic operation resulted in an overflow
Fonctions et propriétés Ranges:
Range.StartAt(N) initialise l’index de début à N, => [N..^0]
Range.EndAt(N) initialise l’index de fin à N => [0..N]
Range.All initialise de début et la fin à 0 => [0..^0] donc tout le tableau
Les itérations
foreach (int item in a[ibegin..iend]){ }
for (int i = 0; i < a[ibegin..iend].Length; i++) {}
Utilisation avec le type Span, ReadOnlySpan, Memory, ReadOnlyMemory:
Vous avez sûrement entendu parler des nouvelles structures, Span, ReadOnlySpan, Memory, ReadOnlyMemory qui permettent de faciliter la manipulation des tableaux et sous tableau, d’une manière plus performantes et plus fiables, on peut désormais initialiser ces structures avec le nouveau type Range: la nouvelle méthode d’extension « AsSpan » accepte en paramètre un type Range :
Span<int> span = a.AsSpan(Range.All); // tout le tableau
Range rg = new Range(3, ^2);
Span<int> span = a.AsSpan(rg); // une partie du tableau
ReadOnlySpan<int> readOnlySpan = a.AsSpan(rg);
Memory<int> memory = a.AsMemory(rg);
ReadOnlyMemory<int> rmemory = a.AsMemory(rg);
La méthode SubString()
Vous pouvez également utiliser le type range pour extraire des sous chaines de caractères d’une manière rapide.
anciennement on utilise la fonction SubString() et on paramètre on lui passe l’index de début et le nombre de caractères à prendre : « Hello World ».Substring(6, 5) ; qui affiche « World »
En revanche avec les Range une légère modification de conception c’est que cette fois-ci on lui passe un index début et index fin, vous pouvez également passer un range en paramètre de la fonction SubString comme vous pouvez utiliser la syntaxe [x..y] directement.
"Hello World".Substring(new Range(6, ^0)); // World
"Hello World"[6..^0]; // World
les Flux asynchrones
L’introduction des flux asynchrone dans le C# 8, est très utile lorsque vous voulez consommer des données dans un flux continue qui provient d’une source asynchrones, exemple les données d’un capteur qui analyses les paramètres du climat, ou des bases de données dans le cloud …etc.
Microsoft a mis en place à travers C# 8 un ensemble de mécanismes permettant la facilité d’implémentations de la création et la consommation des flux asynchrone en ajoutant le mot clé Async pour les méthodes qui retournent un flux asynchrone, et de définir leurs type de retour comme IAsyncEnumerable<T> la version asynchrone de IEnumerable<T> et de retourner les résultats à la demande avec le mot clé « Yield return » .
Et en ce qui concerne la consommation de ce flux on doit itérer avec la boucle foreach sur notre résultat qui est de type IAsyncEnumerable<T> et cela on utilisant le mot clé await avant le foreach
Pour vous donner un exemple, supposant qu’on veut consommer les données d’un capteur météorologique qui nous donne la variation de température, donc c’est purement asynchrone à chaque changement de température .., par conséquent l’utilisation des opérations asynchrone (Asych, await) ici est indispensable sinon, on aura un blocage d’application à chaque fois qu’on attend une réponse de service température.
La création de flux asynchrone :
private static async IAsyncEnumerable<string> GetTemperature()
{
for (int i = 0; i < 10; i++)
{
temperature = temp.Next(10, 16);
delay = waiting.Next(1000, 60000);
await Task.Delay(delay);
if (oldtemp != temperature)
{
oldtemp = temperature;
yield return $"la températeur actuelle est de {temperature} °C";
}
}
}
La consommation de flux :
await foreach (var num in GetTemperature())
{
Console.WriteLine(num);
}
Implémentation par défaut des membres d’interfaces
On connait tous un des principe des interfaces qui dit qu’une interface ne peut pas avoir des implémentations de méthodes, mais ça c’était avant !, avec C# 8 c’est désormais possible d’offrir une implémentation par défaut à des méthodes et des propriétés uniquement !, cela peut semblez dégouttant, mais examinons pourquoi Microsoft à procéder à ce changement.
Aujourd’hui pour créer un code propre maintenable et scalable on doit utiliser des interfaces, ce concept qui permet de définir un contrat entre les classes qui l’implémente.
Imaginant qu’une usine produits des robots avec la fonctionnalité suivante (Move) et qu’une année après l’entreprise à décidé de créer un module d’intelligence artificielle qui permet à tout les robots de parler.
L’entreprise fabrique plusieurs modèles de robots et chaque modèle est fabriqué dans une sous usine avec une équipe dédiée, et que chaque sous usine a accès au module commun de gestion des robots dont l’interface IRobot
|
|||
|
|||
donc vous imaginer les conséquences qu’engendre l’ajout de la méthode « Speak() » dans l’interface, cela effectivement produira beaucoup de changement, car toutes les équipes qui utilisent cette interface devrons, corriger toutes les erreurs de compilateurs en modifiant leurs classes pour implémenter cette nouvelle fonctionnalité et ensuite coder la fonctionnalité pour chaque classe, alors qu’elle est sensée être la même dans tout les modèles de robots et par conséquent elle devra être mutualisé.
C’est pour ça qu’avec C# 8 on peut rajouter la méthode speak() dans l’interface et de lui offrir une implémentation commune par défaut à tout les modèles de robots, et le résultat ça sera juste magnifique car les équipes auront même pas de changement à faire dans leurs codes, et c’est un gain du temps très important, on ai d’accord !, même si y on a qui vont dire qu’on peut faire ça avec une classe abstraite, je dis oui, mais dans un grand projet le temps qu’il faut pour reconstruire avec une classe abstraite est beaucoup plus important que de faire une implémentation par défaut, par contre je vous conseil de ne pas utiliser cette fonctionnalité a tout va, mais plutôt dans des cas où vous auriez pas beaucoup d’autres choix, et dès qu’il y a une autre solution possible n’utilisez jamais l’implémentation par défaut.
Note: pour utiliser l’implémentation par défaut il faut déclarer la variable avec comme type Interface
Exemple:
RobotModele1 robot1 = new RobotModele1();
IRobot robot2 = new RobotModele1();
robot1.Parler() : ça ne marche pas
robot2.Parler() : marche
Les patterns Recursifs
En C# 7 y a eu l’apparition des patterns matching, une technique qui viens de la programmation fonctionnelle comme le F# et qui consiste à donner en entrée une expression et de l’évaluer à travers des motifs (des filtres) pour déduire le modèle ou l’expression correspondante en sortie comme résultat.
dans la programmation C# on peut définir ça comme étant des instructions de sélections (Switch ..case, ou bien plusieurs if else imbriqués)
Le schéma ci-dessous nous montre un exemple d’une syntaxe générique d’un pattren matching : qui veut dire « Matcher l’expression « Expression » avec les motifs (Motif 1, …, Motif N) pour produire un résultat (Expression 1, … Expression N)
matcher <Expression> avec
| <Motif1> -> <Expression1>
| <Motif2> -> <Expression2>
...
| <MotifN> -> <ExpressionN>
Avant on pouvait écrire des patterns simples comme le montre l’exemple suivant
C# 7:
if (liquid is Water wt){ Console.WriteLine($"The name is {wt.Name}"); }
Mais en C# 8 on peut même introduire un autre pattren à l’intérieur de premier pattern, admettant qu’on veut afficher quelques caractéristiques du liquide uniquement si c’est de l’eau et que sa source se trouve dans les Alpes.
On va appliquer l’expression suivante qui va contenir un pattern dans un autre, le premier qui vérifie est ce que le liquide est bien du l’eau et le deuxième vérifie sa source, pour afficher le nom de cette eau et sa température:
C# 8 : (Pattern de proprieties )
if (liquide is Water { Source:"Les Alpes", Name:string name, Temperature:int temperature })
{
Console.WriteLine($"Water name is {name}, his temperature is {temperature}");
}
Et voilà, l’application des patterns sur une arborescence d’objets est trop simplifié et récursive, et c’est très performant !!
Voyant maintenant de coté des expressions Switch là dessous également C# 8 a trop simplifié et réduit la verbosité de ces expressions.
Switch expressions
Switch est généralement utilisé pour remplacer une construction trop répétitives des instructions conditionnelles de If ..Else, avant C# 7 le Switch était limité à des types string et int , long, char, bool, enum, …, mais a partir de C# 7 avec l’apparition des Patterns Matching l’instruction Switch pouvait manipuler d’autres types d’objets, c’est très utile …
Pareil pour les expressions Switch, ils existaient aussi dans C#7, mais apparemment ils sont très verbeuse fastidieuse à écrire, pourtant tout ça pour retourner qu’une seule valeur à la fin, et du coup C# 8 a donnée une version légère simplifié pour l’utilisation des expressions Switch, et casser cette complexité en essayant de faire moins de codes possible.
Voici une petite comparaison entre un Switch case avant et après C# 8 :
private static string SwichV1(Matter matter)
{
switch (matter)
{
case Water eau when eau.Drinkable is true:
return $"L'eau: {eau.Name} qui vient de : {eau.Source} est potable";
case Water eau when eau.Drinkable is false:
return $"L'eau: {eau.Name} qui vient de : {eau.Source} est non potable";
case Water eau when eau is default(Water):
return $"Instance vide !";
case null:
return $"L'objet est null !";
case Petroleum petrole when petrole.Molecule.Name == "H":
return $" Ce pétrole : {petrole.Name} , Contient la molécule : {petrole.Molecule.Name} ";
case Petroleum petrole when petrole.Temperature == 50 && petrole.Flammable:
return $"Attention la température de ce liquide : {petrole.Name} est trop elevé plus il est inflammable";
case Petroleum petrole when petrole.Temperature > 70:
return $"Attention ce liquide {petrole.Name} , très très chaud !";
case Petroleum matiere when matiere is Liquid liquid && liquid is Petroleum petrole:
return $" Cette matière est un liquide ({petrole.Name}), il est produit en : {petrole.ProducingCountry}";
default:
return $"Matière non reconnu !";
}
}
Avec C# 8
private static string SwichV2(Matter matter) =>
matter switch
{
Water { Drinkable: true, Name: string name, Source: string source } eau => $"L'eau: {name} qui vient de : {source} est potable",
Water { Drinkable: false, Name: string name, Source: string source } eau => $"L'eau: {name} qui vient de : {source} est non potable",
Water { } eau => $"Instance vide !",
null => $"L'objet est null !",
Petroleum { Molecule: { Name: "H", Name: string moleculename }, Name: string liquidname } petrole => $" Ce pétrole : {liquidname} , Contient la molécule : {moleculename} ",
Petroleum { Temperature: 50, Flammable: true, Name: string name } petrole => $"Attention la température de ce liquide : {name} est trop elevé plus il est inflammable",
Petroleum { Temperature: int temp, Name: string name } petrole when temp > 70 => $"Attention ce liquide {name} , très très chaud !",
Matter matiere when matiere is Liquid liquid && liquid is Petroleum { Name: string name, ProducingCountry: string pays } petrole => $" Cette matière est un liquide ({name}), il est produit en : {pays}",
_ => $"Matière non reconnu !"
};
Comme vous avez constaté on remarque que le Switch est désormais seul et il ne prend pas de valeurs entre parenthèses, la disparition des Cases : qui sont remplacé par => et le mot clé default remplacé par _ et l’introduction des patterns récursives qui inspectent les propriétés de l’objet (pattern de propriétés), tout ça rend la construction plus lisible et élégante, avec moins du code, n’est ce pas !
Et comme vous remarquer aussi vous pouvez utiliser des patterns vides ou null de cette manière :
Water {} eau => $"Instance vide !",
null => $"L'objet est null !",
C’est très pratique lorsqu’on veut éviter le maximum d’erreurs à l’exécution des patterns
Patterns Positional
A partir de C# 7, vous pouvez écrire des méthodes « Déconstructeur » (Deconstruct) dans des classes, elles permettent de faire déconstruire un objet dans les variables tuple leurs syntaxe est définit comme suite :
public void Deconstruct(out string name, out string? color, out bool flammable) => (name, color, flammable) = (Name, Color, Flammable);
En C# 8 les patterns bénéficient de cette méthode pour construire ce qu’on appelle des patterns positional en s’appuyant sur les méthodes « Deconstruct » : dans l’exemple ci-dessous en instancie des objets liquides qu’on passe dans notre méthode « Show » qui permet de deviner le type de liquide en le déconstruirons :
static string Show(Liquid liquid) => liquid switch
{
("Eau",null,false) => "Is Water !",
(var x, var y, var z) when z is true => $"Is Flammable !",
_ => "unknown"
};
Patterns Tuples
Ils sont très similaire aux patterns positional cité ci-dessus, ils permettent de tester plusieurs éléments en même temps en se basant sur la syntaxe d’un tuple, voici une illustration dans l’exemple ci-dessous : qui permet d’afficher un message d’erreur suivant un tuple (erreur, code d’erreur)
static string ShowError(string erreur, int code) =>
(erreur, code) switch
{
("File Not Found", 1) => "Error with code 1, filesystem",
("Index Out Of Range", 2) => "Error index out, array",
(null, 0) => "No Error",
(_, _) => "unknown"
};
Déclarations using
Vous avez marre des accolades à chaque fois que vous coder un bloque Using ? et surtout lorsque ces bloques sont imbriquées comme dans l’exemple ci-dessous
using (ClassA a = new ClassA())
{
using (ClassB b = new ClassB())
{
using (ClassC c = new ClassC())
{
// instructions
}
}
}
La multiplicité des accolades devient alors trop important ce qui rend le code moins lisible.
En C#8 l’écriture des bloques Usings à été simplifié en enlevant les accolades, mais je sais que beaucoup d’entre vous se pose la question suivante : dans les bloques usings avec l’ancienne écriture on sait que les objets seront disposé à la fin de l’accolade de bloque correspondant, mais là avec cette écriture simplifié on ne voit pas trop comment les objets seront disposés ?
Une bonne question effectivement, mais au lieu de disposer l’objet à la fin de l’accolade de Using correspondant, on le dispose à la fin contexte dans lequel l’objet à été instancié.
Exemple : si on a une fonction qui écrit un flux dans un fichier texte comme suite :
private static void WriteInFile()
{
string filepath = @"C:\_DEV\UsingsDeclaration\file.txt";
using StreamWriter stream = new StreamWriter(filepath, true);
}
comme l’objet stream est instancié à l’intérieur de la fonction WriteInFile() alors il sera disposé à la fin de l’accolade de cette fonction.
Fonctions locales (statiques)
Les fonctions locale introduites dans C# 7 sont un outils très puissant, pour faire des mini fonctionnalités à l’intérieur d’une fonction mère, tout en pensant bien-sûr à respecter les principes solides et de ne pas tomber dans une fonctions coteau suisse avec plusieurs fonctions locales divergentes, par contre faire des fonctions à usage locale en respectant l’orientation de la fonction globale est une bonne pratique et cela évite également qu’un autre développeur puisse appeler une fonction local par erreur, car elle est encapsulé .
Parmi les contraintes d’utilisation des fonctions statiques, vous ne pouvez pas ajouter un modificateur d’accès car elles sont privées par défaut, et aussi vous ne pouvez pas appliquer le mot clé Static
Avec C# 8 vous pouvez désormais utiliser le mot clé Static sur les fonctions locales, par contre elles ne peuvent pas accéder à des variables qui sont dans la portée de la fonction globale, et cela c’est un avantage car par souci de performances, une fonction locale doit utiliser ses propres variables
Voir l’exemple ci-dessous :
|
|
Résultat : pour (2,2)
Add = 4 , Sub = -1, second param increment = 3 // fonction non statique => résultat non correcte.
Add = 4 , Sub = 0, second param increment = 3 // fonction statique => résultat correcte
Je trouve que les fonctions statique locale sont un bon moyen pour éviter que les développeurs puissent manipuler les variables de la fonction globale justement dans le corps de la fonction locale en raison de performances, comme en vient de le voir dans l’exemple précédent, et puisque Les méthodes statiques ne peuvent pas accéder à des champs non statiques ni a une variable d’instance d’un objet quelconque dans leur type contenant, cela nous oblige à passer les variables globales en paramètres.
Vous allez me dire qu’on peut aussi dans le cadre des fonctions non static utiliser des paramètres. Oui, sauf que ce n’est pas obligatoire , en revanche les fonctions static c’est une obligation si vous voulez manipuler les variables globales, le compilateurs vous génère l’erreur suivante « a static local function cannot contain a reference to <variable> » donc vous n’avez pas le choix , et tant mieux !
Autres fonctionnalités
Certaines de ces fonctionnalités que je citerai ci-dessous ne sont pas encore finalisées ils devront être publiée dans des versions ultérieures de C#8 c’est-à-dire C#8.X
Target-typed new-expressions (les types ciblés)
Target-typed new-expressions : c’est une nouvelle fonctionnalité dans C#8 qui vous permet d’omettre le type de l’objet au mot clé New lorsque vous instanciez ce dernier, c’est à dire dans ma classe Liquid , je peux instancier un liquide de cette façon:
Liquid liquid = new { Name = « Lat », Color = « White », Flammable = false };
Alors qu’anciennement on le fait comme ça :
Liquid liquid = new Liquid { Name = « Lat », Color = « White », Flammable = false };
Le compilateur est capable d’inférer le type de la partie gauche de la déclaration de l’objet, par conséquent on aura moins du code à écrire.
Ref Struct
Pour accéder à la mémoire non managé , C# 7 à introduit les ref struct qui est une structure qui sera alloué uniquement sur la pile. Span<T> est un exemple de ce type qui fournit des performances très hautes car il gère accès automatiquement à la mémoire managé et non managé et la pille, par conséquent lorsqu’on manipules des chaines de caractères ou des tableaux de grands volumes de données avec Span ou ReadOnlySpan on garantira plus de performances dans la gestion de la mémoire, cependant une petite limitation qui s’impose, c’est que ces types ref struct il ne peuvent pas être disposé on utilisant le « Using » car ils ne peuvent pas implémenter des interfaces. C’est la raison pour laquelle C# 8 à permet aux ref struct d’implémenter l’interface IDisposable, voici un exemple :
ref struct Liquid : IDisposable
{
public void Dispose()
{
}
}
using (var liquid = new Liquid ()) {.. } // liberation de resources...
L’attribut CallerArgumentExpression
A des fins de diagnostiques, C#8 veut ajouter l’attribut « CallerArgumentExpression » dans le namespace System.Runtime.CompilerServices, afin qu’une méthode appelée puisse recevoir plus d’informations sur celui qu’il a appelée (appelant), pour rappel : dans la version C# actuelle il y a déjà les attributs suivant : (CallerMemberName, CallerFilePath et CallerLineNumber)
Déconstruction du littéral « default »
Pour initialiser une variable à une valeur par défaut on utilise le mot clé « default », le mécanisme est de même pour les Tuples également.
Avant C# 7 on peut initialiser un tuple de cette façon : On donne à chaque type sa valeur par défaut
(int x1, string y1) = (default(int), default(string));
Ensuite en C# 7, y avais une amélioration pour supprimer les types et de laisser juste les mots clé default
(int x2, string y2) = (default, default);
Et désormais en c# 8, un seul default peut initialiser un tuple.
(int x3, string y3) = default;
Operateur ??=
Si vous voulez initialiser une variable lorsqu’elle est null avant C# 8 vous tester avec le mot clé if
if (value == null){value = "Hello World!"; }
Avec le mot clé ??= le code est plus simple exemple : value ??= « Hello World! »;
Changement de l’ordre dans les opérateurs $,@
Pour la mise en forme des chaines de caractère C# 6 à introduit l’opérateur $ utilisé pour remplacer string.Format, mais dans une chaine à qui on veut rajouter l’opérateur @ pour interpréter textuellement les séquences d’échappement on aura les 2 opérateurs cote à cote : $@
Dans C# 7 : la syntaxe était d’écrire le $ suivi de @
var filePath = $@"c:\MyFolder\{file}";
En C# 8, c'est l'inverse le @ suivi de $
var filePath = @$"c:\MyFolder\{file}";
Utiliser des attributs sur des classes génériques :
Les attributs sont des moyens plus efficaces pour ajouter des métadonnées à nos classes, cependant on ne pouvait pas créer des attributs génériques, et le but de cette nouvelle fonctionnalité est de remédier à cela :
public class GenericValidateAttribute<T> : Attribute { }
[GenericValidate<int>]
public class ClassToValidate { }
Les Records (enregistrement)
C’est un nouveau format de déclaration de classe et des structures simplifiée, qui permet aussi d’intégrer un certain nombre de fonctionnalité, à partir d’une déclaration de base, le compilateur est donc sensé travailler avec une classe plus détaillée générée par lui, voici un exemple :
public class Liquid(string Name, string Color);
le compilateur génère pour lui une classe beaucoup plus grande qui implémente également IEquatable et qui contient des méthodes telles que GetHashCode(), Equals(), Deconstruct()… etc.
public class Liquid: IEquatable<Liquid>
{
public string Name { get; }
public string Color { get; }
public Liquid(string Name, string Color)
{
this.Name = Name;
this.Surname = Color;
}
public bool Equals(Liquid other) { return Equals(Name, other.Name) && Equals(Color, other.Color); }
public override bool Equals(object other) { return (other as Liquid)?.Equals(this) == true; }
public override int GetHashCode() { return (Name.GetHashCode() * 17 + Color.GetHashCode()); }
public void Deconstruct(out int Name, out int Color) { Name= this.Name; Color= this.Color; }
public Liquid With(int Name = this.Name, int Color= this.Color) => new Liquid(Name, Color);
}
Extension de Tout
Aujourd’hui vous pouvez faire des extensions des objets on utilisant une classe statique et une méthode statique par exemple faire une extension à la classe string pour ajouter une méthode ça donne ça :
public static class MyString
{
Public static int NewMethode(this string){.. do somthigs..}
}
Pour pallier aux limitations présentes dans cette technique C# 8 veut introduire une nouvelle manière d’étendre les objets avec une autre syntaxe complètement différente, et qui facilitera aux développeurs l’extension des méthodes et également des propriétés et autres choses …
public extension MyString extends String
{
public int NewMethode(){.. do somthigs..}
}
Avec l’utilisation de mot clé extends qui veut dire étendre quelques choses, et le mot clé extension
Dépendances de plateforme
la plupart des nouvelles fonctionnalités de c#8 sont introduite dans la nouvelle version de .Net Standard2.1 , .NET Core 3.0 ainsi que Xamarin, Unity et Mono implémenteront tous .NET Standard 2.1, mais pas le .NET Framework 4.8, ce qui explique que de nombreuses de ces nouvelles fonctionnalités ne fonctionnerons pas sur le .NET Framework 4.8
Migration
L’une des fonctionnalités de C# 8 qui fait un pas majeur par rapport aux versions précédentes c’est évidement les références null, car en point de vu de migration de code c’est elle qui va générer plus d’efforts de migration que d’autres fonctionnalités, et cet effort de migration on peut le mesurer par le nombre de Warnings qui seront générés suite à l’activation de cette fonctionnalité sur un ancien code, par conséquent il y a une équation exponentielle ,plus le projet est volumineux plus le nombre de warnings générés est volumineux, donc il est claire que le travail de migration qui sera fournit dans les cas des petits projets est minime, mais il peut s’avérer de grandes envergures en ce qui concerne les grands projets d’entreprises et surtout les projets qui ont passés moins d’efforts de conceptions , c’est pour ça , la manière la plus adapté pour la migration de grands projets consiste à migrer par sous projets et dans chaque sous projet migrer modules par modules,
Exemple : on va essayer de migrer un code très simple : ci-dessous une classe Liquid
public class Liquid
{
public Liquid(string name, string color, bool flamable)
{
Name = name;
Color = color;
Flammable = flamable;
}
public string Name { get; set; }
public string Color { get; set; }
public bool Flammable { get; set; }
}
Et une méthode qui calcule l’index d’une couleur avec une formule.
static void Main(string[] args)
{
Liquid eau = new Liquid("eau", null, false);
int index = GetColorIndex(eau);
Console.WriteLine("Hello World!");
}
private static int GetColorIndex(Liquid liquid)
{
return liquid.Color.Length * 10;
}
1- on doit commencer notre migration par l’activation des référence null dans notre projet, pour cela il faut aller dans le fichier csproj et de rajouter la ligne suivante : <NullableContextOptions>enable</NullableContextOptions> cette ligne permet d’activer le control des références null dans notre projet, et si vous voulez le désactiver mettez disable :<NullableContextOptions>disable</NullableContextOptions>, voila c’est plutôt pas mal, il faut juste savoir qu’on faisant cela on activera la fonctionnalité sur l’ensemble de notre projet, donc comme j’ai dit auparavant si vous avez un grand projet vous risquez de voir beaucoup de Warnings à la fois !, ce qui rends la migration complexe, ce que je vous conseil dans ce cas, c’est d’utiliser plutôt les directives suivantes #nullable enable et #nullable restore pour entourer le module ou la classe que vous voulez migrer …c’est plus simple
#nullable enable
// notre code à migrer...
#nullable restore
Et vous déclenchez des Warnings juste sur cette partie du code ; poursuivant notre migration :
2- Liquid eau = new Liquid("eau", null, false); on instancie un objet eau avec une couleur null, (l'eau n'a pas de couleur)
3- le compilateur n’est pas content car la propriété Color n’est pas nullable, il génère un Warning (cannot convert null literal to non-nullable reference or unconstrained type prameter) sur le null
4- Donc on corrige la propriété Color à nullable : public string? Color { get; set; }
par conséquent on exprime notre intention qu’il existe des liquides sans couleurs comme l’eau .
5- on corrige également le paramètre color de constructeur pour le mettre aussi à nullable.
6- C’est bien, mais encore une fois le compilateur nous prévient, avec un Warning (possible dereference of null reference) si on corrige pas ce Warning y a une possibilité d’avoir une exception à l’exécution de GetColorIndex()
, car on a déclaré la « Color » nullable.
7- on peut corriger ça avec plusieurs façons, notamment de vérifier si la couleur est non null, ou bien de rajouter tout simplement l’opérateur ? (liquid.Color?.Length).
8- on peut utiliser l’opérateur « ! » pour corriger le Warning, mais dans ce cas vous dites au compilateur ne t’inquiètes pas on est sûr de nous, on ne va pas recevoir une valeur null !!, mais je ne vous conseil pas de le faire comme ça, sauf dans le cas où vous implémentez vous même une fonction de tests de non null par exemple :
if(!IsNull(liquid.Color)) return liquid.Color.Length * 10;
dans ce caS là vous êtes sûr que vous vérifiez bien les null dans la fonction IsNull(), et par conséquent le compilateur n’est pas capable de deviner ce que vous faites et là vous pouvez le rassurer on ajoutant l’opérateur ! (liquid.Color!.Length )
et vous prenez votre responsabilité en cas d’exceptions. Une fois migré vous allez diminuer énormément les exceptions Reference Null, c’est génial n’est ce pas !
C # 8.0 est enfin disponible en « Preview » dans Visual Studio 2019, ou VSCODE, Sa version finale sera publiée avec .NET Core 3.0. Malheureusement toutes les fonctionnalités ne seront pas disponibles dans le Framework .NET, comme Les flux asynchrone, les ranges et Indexes qui dépendent des types qui ne seront ajoutés qu’aux plates-formes .NET compatibles avec .NET Standard 2.1.
Je pense que les fonctionnalités les plus importantes sont les types de référence nuls et les améliorations des patterns matching, car ils vont aider les développeurs et concepteurs à créer des applications plus fiables avec un code plus lisible, donc plus maintenable, cependant l’enjeu de migration est très grand notamment avec les types références null. À noter aussi que parmi les fonctionnalités cités et autres y on a beaucoup qui vont attendre les versions ultérieurs de C#8.