Nous avons vu avec Romain les grandes nouveautés du C# 9 dans la précédente partie de notre tour d’horizon du langage. Dans cette partie nous allons nous concentrer sur des améliorations de features déjà présentes et sur de petites nouveautés marginales mais qui peuvent être fort utiles dans certains cas ou qui améliorent tout simplement l’écriture de nos algorithmes.
Nous allons aborder ici les nouveautés suivantes :
- Les améliorations du Pattern Matching
- L’introduction du retour covariant
- Les améliorations apportées aux méthodes partielles
- Les autres petites nouveautées
À noter que ces nouveautés du langage sont accessibles dans la RC 2 du framework .net 5. Cette version est dite « Go Live« , elle intègre toutes les nouveautés du framework et de C# 9 (F# 5 est aussi de la partie). Lors de son passage en version finale le framework .net 5 ne recevra que des correctifs. Cette Release Candidate 2 peut donc être utilisée pour préparer la migration de ses applicatifs vers cette nouvelle version ou en Production, sa stabilité étant assez éprouvée.
Améliorations du Pattern Matching
Démarrons par les les améliorations apportées au pattern matching, en C#9 deux grosses nouveautés font leurs apparitions :
- Les patterns relationnels : >, <=, =
- Les patterns logiques : and, not et or
Cela va nous permettre de filtrer plus finement nos cas pour le switch en combinant les conditions et en supportant tout ce que peut faire l’instruction if mais avec une syntaxe différente au lieu de && ce sera and, pour || ce sera or, etc …
En C# 9, l’écriture de pattern de type a été simplifiée. Nous ne sommes plus obligés de déclarer un identifieur, nous pouvons spécifier seulement un type et notre cas seras toujouts valide. Nous allons à travers un exemple passer en revu ces nouveautés. Nous partons d’une expression switch classique C# 7 et nous verrons comment ajouter des possibilités à notre logique avec les améliorations introduites par les versions 8 (l’actuelle) et 9 (qui sortira en même temps que .net 5, c’est à dire pour la fin d’année 2020 en version finale).
Voici notre switch C# 7 de départ ainsi que l’objet que nous allons filtrer dans nos différentes expressions switch :
var inspectedBrain = new Brain { Age = 35, ADN = "kC7Ly4MSQD9hRxPiBr9Pn6X6696KRKsWx9HBS4FjbLgCCVta2fuE4C4mY8aWn5zJ", Learnings = new List<string> { "Langage Français", "Langage Anglais", "Programmation en C#", "La cuisine", "La physique quantique" } }; // C# 7 switch string result; switch (inspectedBrain.ADN) { case "kC7Ly4MSQD9hRxPiBr9Pn6X6696KRKsWx9HBS4FjbLgCCVta2fuE4C4mY8aWn5zJ": result = "ADN humaine."; break; case "--------": result = "C'est une espèce peu évoluée."; break; default: throw new Exception("Unknown ADN."); };
En C# 8 les expressions switch on été intégrées. On gagne donc en clarté. Pour des contrôles rapides sur une variable cette nouvelle notation est très efficace. Voici comment on réécrit notre précédent switch pour le transformer en expression switch :
// C# 8 switch expressions result = inspectedBrain.ADN switch { "kC7Ly4MSQD9hRxPiBr9Pn6X6696KRKsWx9HBS4FjbLgCCVta2fuE4C4mY8aWn5zJ" => "ADN humaine.", "--------" => "C'est une espèce peu évoluée.", _ => throw new Exception("Unknown ADN."), };
En C# 9 nous allons pouvoir aller encore plus loin dans les contrôles effectués sur notre objet Brain. Dans un premier temps voici un nouveau contrôle qui peut être fait grâce à l’introduction des patterns logiques et relationnels :
// Relationnal Pattern & Logical pattern static string GetAgeRange(int age) => age switch { <= 10 => "Enfant", > 11 and < 18 => "Adolescent", (> 18 and < 30) or 110 => "Jeune Adulte (ou un très vieux...)", > 30 and < 95 => "Adulte", _ => "Impossible de déterminer la tranche d'âge." };
On voit ici que l’on peut combiner très facilement ces deux nouveautés mais on peut pousser le contrôle sur notre objet encore un peu plus loin en le combinant avec les patterns de type. Dans ce dernier exemple, nous combinons tous les patterns possibles au sein d’une expression switch, mais cette fois-ci, nous acceptons tous types d’objets en entrée.
static string InspectSomething(object something) => something switch { Brain b when b.Learnings.Count == 8 || b.ADN.Length >= 450001154 => "Cerveau de génie", Brain b when b.Learnings.Count >= 4 && b.ADN.Length > 6 => "Cerveau en pleine forme !", Brain { ADN: "--", Age: 0, IsSmart: false } => "Cerveau de pigeon", string => "C'est une chaîne de charactères pas un cerveau !", not null => "truc inconu non null" };
On remarquera que pour notre cas string on ne déclare pas d’identifieur cela est dorénavant permis en C# 9. Pour les plus observateurs d’entre vous, vous aurez sans doute remarqué que dans nos deux premiers cas nous n’utilisons pas la notation and ni or. En effet nous ne pouvons pas combiner les patterns de type et les patterns logiques au sein d’un même case.
Toutes ces nouveautés combinées ensemble permettent de finement filtrer ses cas sans compromettre la lisibilité du code car ces instructions peuvent se lire comme de « vraies phrases ». C# 9 nous facilite encore une fois la vie pour développer nos algorithmes en ayant étendu les possibilités des expressions switch.
Retour covariant
Le retour co-variant va nous apporter un peu plus de souplesse dans l’héritage. Cette fonctionnalité permet à une classe surchargeant une méthode de modifier le type de retour de celle-ci si il dérive du type déclaré dans la définition originale de la méthode.
Cette nouveauté peut notamment être utile pour l’implémentation d’une factory car elle va nous permettre de retourner le type final. Nous ne serons plus obligés de retourner le type de base. Cela facilitera l’utilisation des méthodes que nous pourrions avoir définies uniquement pour ce type.
Nous poursuivons,
,, sur notre thématique des cerveaux mais cette fois-ci nous allons utiliser une abstract factory pour créer des cerveaux. Ci-dessous les définitions de nos classe de base. Nous commençons par nos classes de bases BrainBase et la définition de notre abstract factory.
public interface IBrain { void ProcessInformation(IList<string> information, bool remember); public void Learn(IList<string> knowledge) => ProcessInformation(knowledge, true); }
public abstract class BrainBase : IBrain { protected List<string> Learnings { get; } = new List<string>(); public BrainBase() => Learnings.AddRange(new[] { "Parole", "Lecture", "Mémorisation" }); public abstract IEnumerable<Information> ReturnLearnings(); public virtual void ProcessInformation(IList<string> information, bool remember) { foreach (var i in information) if (remember) this.Learnings.Add(i); } }
public interface IBrainFactory { T CreateBrain<T>() where T : BrainBase; }
public abstract class BrainFactoryBase : IBrainFactory { public T CreateBrain<T>() where T : BrainBase { if (CreateBrain() is not T brain) throw new ArgumentException($"This factory cannot instantiate the Type : {typeof(T).FullName}"); return brain; } public abstract BrainBase CreateBrain(); }
Nous venons de définir une classe de base contenant la logique d’un cerveau qui apprend, retient et restitue les connaissances. Nous allons maintenant définir deux implémentations de cette classe de base qui décrit le fonctionnement minimal des cerveaux. Ils seront instanciés par la suite avec des implémentations de notre abstract factory : BrainFactoryBase.
Attardons-nous un petit peu sur notre factory de base. Elle implémente le contrat définit dans l’interface IBrainFactory et appelle sa méthode abstraite CreateBrain pour instancier les cerveaux. Cette dernière sera surchargée dans les implémentations de cette factory. Au passage vous pourrez remarquer l’utilisation du « not » dans notre if comme dans les pattern logiques.
public class HealthyBrain : BrainBase { public override IEnumerable<UnderstandableInformation> ReturnLearnings() => Learnings.Select(l => new UnderstandableInformation(l)); }
public class SickBrain : BrainBase { public void ClearLearnings() => Learnings.Clear(); public override IList<CorruptedInformation> ReturnLearnings() => Learnings.Select(l => new CorruptedInformation()).ToList(); }
Nous avons ici deux implémentations de notre BrainBase qui sont assez différentes :
- un cerveau sain qui retourne un IEnumerable<UnderstandableInformation>
- un cerveau malade incapable de restituer des connaissances et qui retourne une IList<CorruptedInformation>
C’est ici notre première utilisation du retour covariant car ces deux méthodes sont des surcharges de la méthode définie dans BrainBase. Les classes CorruptedInformation et UnderstandableInformation héritant de la classe Information, cette surchage est donc possible. C’est aussi le cas pour IList qui hérite de IEnumerable. Terminons notre exemple en créant nos factories.
public class HealthyBrainFactory : BrainFactoryBase { public override HealthyBrain CreateBrain() => new HealthyBrain(); }
public class SickBrainFactory : BrainFactoryBase { public override SickBrain CreateBrain() => new SickBrain(); }
var goodFactory = new HealthyBrainFactory(); var badFactory = new SickBrainFactory(); HealthyBrain brain1= goodFactory.CreateBrain(); SickBrain brain2 = badFactory.CreateBrain(); brain1 = (goodFactory as IBrainFactory).CreateBrain<HealthyBrain>(); brain2 = (badFactory as IBrainFactory).CreateBrain<SickBrain>(); // Throw ArgumentException => This factory cannot instantiate the Type SickBrain SickBrain nullBrain = (goodFactory as IBrainFactory).CreateBrain<SickBrain>();
Ces dernières prennent partie du retour covariant pour retourner le type final. Dans notre implémentation, on peut donc facilement et directement obtenir un type concret à partir de notre factory mais aussi via l’interface en spécifiant le type désiré. Nous avons vu ensemble une utilisation concrète de cette nouveauté. C’est maintenant à vous de jouer pour lui trouver une place douillette dans vos algorithmes !
Améliorations des méthodes partielles
Avec l’arrivée de C# 9 les méthodes partielles font tomber quelques limitations :
- Obligation de retourner void
- Impossibilité de prendre un paramètre out
- Impossibilité de spécifier un modificateur d’accessibilité
Du coup vous vous demandez ce que peuvent bien être ces méthodes partielles. Et bien, c’est le même principe que pour les classes partielles (une méthode partielle ne peut d’ailleurs être déclarée qu’au sein d’une classe partielle). Elles permettent d’écrire la signature dans un fichier et de l’implémenter dans un autre. En C# 9 elles ont dorénavant les mêmes possibilités qu’une fonction classique. Vous allez me dire : « À quoi cela peut bien servir ? » Et bien, cela est très souvent utilisé dans les outils qui génèrent du code (je vous invite à regarder de plus près vos classes de migration EF Core qui sont des classes partielles). Cela permet d’avoir d’une part le code autogénéré et d’autre part le code écrit par le dévelopeur sans que l’un n’écrase l’autre à chaque nouvelle génération du code. Les méthodes partielles sont donc utiles, entre autres, pour appeler du code écrit part le développeur au sein du code auto-généré.
Voici un exemple pour illustrer ces modifications. Ici, nous allons imaginer qu’un outil (T4 par exemple) nous a généré le fichier Brain.Designer.cs qui contient la première partie de l’implémentation de notre cerveau. Nous allons ensuite définir le reste de notre logique dans un autre fichier Brain.cs car en l’état cela ne compilerait pas. Les méthodes partielles utilisant ces nouveautés du C# 9 doivent obligatoirement être implémentées à l’inverse des méthodes partielles C# 8 (c-à-d retournant void et n’utilisant pas de paramètre out). Des erreurs de compilation on été ajoutées pour chacun de ces cas, afin de nous avertir si des méthodes partielles requises n’ont pas de corps défini. Par contre si nous n’implémentons pas les méthodes partielles retournant void, elles seront supprimées à la compilation ainsi que tous les appels à ces méthodes.
// Logique de cerveau autogénéré par T4 public partial class Brain { private int _totalInformationsSent; // Méthodes partielle C#8 partial void TreatNonsense(IEnumerable<Information> informations); partial void OptionnalCheck(IList<Information> informations); // Nouveauté C#9 protected partial bool TreatInformationSeen(IEnumerable<Information> informations); public void SendInformations(IList<Information> informations) { _totalInformationsSent += informations?.Count ?? 0; OptionnalCheck(informations); if (informations == null) return; TreatNonsense(informations.Where(i => i.DataType == DataType.Nonsense)); var hasLearn = TreatInformationSeen(informations.Where(i => i.DataType != DataType.Nonsense)); if (hasLearn) Console.WriteLine("Le cerveau a amélioré ses connaissances !"); } }
Nous passons maintenant à l’implémentation qui reste à notre charge. Nous devons obligatoirement implémenter la méthode TreatInformationSeen et nous pouvons décider d’implémenter une ou les deux méthodes partielles C# 8 TreatNonsense et OptionnalCheck. Nous allons donc imptémenter TreateInformationSeen qui va filtrer les informations en fonction de leur type et ne garder que les informations utiles. Et nous n’implémentons aucune des autres méthodes partielles pour le moment. Nous n’avons pas de traitement particulier à effectuer en plus (avec OptionnalCheck) et comme tout bon cerveau, nous ne voudrions pas prendre en compte des informations inutiles (avec la méthodes TreatNonsense).
// Lobe Frontal public partial class Brain { public List<Information> UsefullInformations { get; } = new List<Information>(); protected partial bool TreatInformationSeen(IEnumerable<Information> informations) { var actualKnowledge = UsefullInformations.Count; var informationsToRemember = informations.Where(i => i.DataType == DataType.Information).ToList(); UsefullInformations.AddRange(informationsToRemember.Where(i => !UsefullInformations.Any(ui => ui.Content == i.Content))); return UsefullInformations.Count > actualKnowledge; } }
Au travers de ces ajouts, les méthodes partielles se comportent dorénavant comme des méthodes classiques, en tout cas dans la déclaration de leur signature, hormis le mot clé partial. Cela permet entre autre d’étendre les possibilités des outils générant du code.
Les autres nouveautés
Nous allons aborder ici les autres petites nouveautés introduites par C# 9. Celles ci sont beaucoup moins impactantes, mais peuvent être fort pratiques dans certains cas particuliers, ou tout simplement nous faire gagner du temps.
Support des attributs sur les méthodes locale
Introduites en C# 7 les fonctions locales sont une feature du langage assez peu connue, mais peuvent être particulièrement utiles dans des méthodes qui itèrent sur des éléments. Dans cette nouvelle version du langage, ces méthodes supportent maintenant les attributs, ce qui étend encore un peu plus leurs possibilités.
static void Main(string[] args) { [Conditionnal("Debug")] static void DebugAction(int ms, string action) { Console.WriteLine($"The action '{action}' took {ms} ms to complete."); } // ... Exécution de l'action DebugAction(timer, "Démo"); }
Dans l’exemple ci-dessus, nous avons déclaré une méthode locale DebugAction qui va nous permettre de logger dans la console. En combinant cela avec l’utilisation de l’attribut Conditionnal, nous indiquons que cette méthode doit être intégrée uniquement si nous avons la variable d’environnement « DEBUG ». Cet attribut est un équivalent de la notation suivante :
#IF DEBUG .... #ENDIF
Le paramètre abandon pour les expressions lamda
Comme son nom l’indique (Lambda discard parameter pour les anglophones) ce nouveau paramètre va nous permettre d’ignorer un paramètre d’entrée d’une expression lambda lors de sa définition. Ce discard parameter est très semblable à celui que l’on trouve en Typescript en terme d’utilisation.
List<Func<int, int, string>> funcs = new List<Func<int, int, string>>(); funcs.Add((_, _) => "Aucun paramètres ne m'est utile."); funcs.Add((int a, int _) => $"Le premier paramètre est {a}"); funcs.Add((int a, int b) => $"le premier paramèter est {a} et le second {b}");
Avec ce paramètre, les entiers passés en entrée ne seront pas disponibles dans la définition de l’expression lambda concernée. Cela nous offre en somme la possibilité de créer des lambdas avec des paramètres « optionnels » tout en possédant la même signature. Cette évolution est très intéressante dans le contexte d’une utilisation de RX.Net ou plus généralement en appliquant les concepts de la programmation reactive.
Typage des expressions ternaires
On est ici sur une petite amélioration qui va nous permettre d’éviter les conversions explicites dans les expressions ternaires. Pour les amateurs de cette notation c’est une super nouvelle. C’était quand même frustrant de devoir convertir explicitement les deux côtés de l’expression quand les types dérivaient du même. Cela nous fait gagner en lisibilité :
// Post C#9 Stream s = inMemory ? (Stream)new MemoryStream() : (Stream)new FileStream("path", FileMode.Open, FileAccess.Read); // En C# 9 Stream s = inMemory ? new MemoryStream() : new FileStream("path", FileMode.Open, FileAccess.Read);
Nous avons vu au travers de tous ces exemples les petites nouveautés de cette dernière mouture du C# qui nous simplifient l’écriture d’algorithmes ou qui nous donnent plus de possibilités. Nous verrons dans un prochain article les nouveautés du serializer JSON de Microsoft qui souhaite pousser sur le banc de touche le bien connu de tous JSON.net (par Newtonsoft).
0 commentaires