Dans les derniers messages, j'ai lancé un défi pour modifier la sortie du programme suivant pour afficher uniquement les lignes avec des nombres pairs (allez lire les parties I et II si vous ne l'avez pas déjà fait).
- l'utilisation de System.Runtime.CompilerServices qui n'est pas nécessaire car nous ne référençons aucun type déclaré dans cet espace de noms.
- le if sur la ligne 5 de la méthode Foo() qui, clairement n'est pas nécessaire car le programme ne transmet que des chaînes non nulles/vides à cette méthode.
Comme je l'ai laissé entendre, la solution que j'ai trouvée explore C# interpolated string handlers, une fonctionnalité introduite dans C#10.
Avec cet information la, nous pouvons atteindre notre objectif en simplement remplacent le type de paramètre msg par un interpolated string handler personnalisé (InterpolatedHandlerTrick dans le code ci-dessous) comme suit:
- Déclarez un type et ajoutez-y l'InterpolatedStringHandlerAttribute (ligne #14)
- Implémenter un constructeur prenant au moins deux integers (literalLength et formattedCount) (notre implémentation déclare des paramètres supplémentaires pour profiter de certaines fonctionnalités plus avancées, qui seront discutées plus tard)
- Mettre en œuvre les méthodes
- void AppendLiteral(string msg)
- void AppendFormatted<T>(valeur T)
C'est tout belle et bien mais la question intéressante est : pourquoi ça marche ?
Si vous collez ce code dans sharplab.io et sélectionnez C# dans le menu déroulant Résultats, vous verriez que le compilateur C# a réécrit votre programme (ou, pour s'en tenir à la terminologie technique de cette transformation, le compilateur a réduit le code), plus précisément l'appel à la méthode Foo() à quelque chose comme :
Notez que le compilateur a introduit une variable locale typée comme notre interpolated string handler (ligne 4) et a effectué des appels de méthode dessus en passant les différentes parties de la chaîne interpolée ; fondamentalement, il l'a divise sur les limites `{...}` et pour chaque partie, il a appelé handler.AppendLiteral(part); (ligne 7) suivi pour handler.AppendFormatted(contents of {...}) (ligne 8) .
Notez également que ces appels sont enveloppés dans un if (ligne 5) contrôlé par une variable qui a été initialisée par le constructeur de notre handler et voilà, nous avons tous les éléments dont nous avons besoin pour implémenter le comportement souhaité ; la méthode prenant notre interpolated string handler spécifie qu'un ou plusieurs de ses paramètres (dans ce cas un seul) doivent être passés au constructeur de notre gestionnaire de chaîne (via InterpolatedStringHandlerArgumentAttribute) qu'il utilise pour décider si cette chaîne doit être traitée ou non par définir un paramètre ref bool (déclaré comme dernier paramètre du constructeur) conduisant le code réduit à ignorer les appels aux méthodes AppendX() et ainsi l'instance du gestionnaire de chaîne passé à Foo() produit une chaîne vide qui échoue la condition et saute Méthode Console.WriteLine() !
Cool, mais avant de vous quitter, voici quelques améliorations/considérations de performances que cette fonctionnalité apporte à la table :
- DefaultInterpolatedStringHandler est, comme son nom l'indique, utilisé chaque fois qu'une chaîne interpolée est transmise à une méthode qui prend une string comme paramètre, donc, simplement en recompilent votre code basé sur C# 9 avec C# 10 devrait vous apporter au moins quelques améliorations de performances/allocations.
- Étant donné que DefaultInterpolatedStringHandler est déclaré en tant que structure ref, son instanciation ne provoquera pas d'allocation de tas (en général, tous les gestionnaires de chaînes interpolées doivent être une structure ref pour éviter de telles allocations).
- Étant que String.Format() n'est plus utilisé, aucun array n'est alloué.
- Les allocations peuvent être complètement évitées lorsque l'argument spécifié via InterpolatedStringHandlerArgumentAttribute définit que la chaîne résultante ne sera pas utilisée (très utile dans la journalisation et le code d'assertion)
- Sachez que si le gestionnaire de chaîne interpolée signale que la chaîne résultante ne sera pas utilisée (voir le point ci-dessus), aucun effet secondaire lié à l'expression dans cette partie de la chaîne interpolée ne sera observé ; par exemple, si nous changeons la valeur interpolée dans le puzzle (ligne 10) de $"Test {i}" à $"Test {SomeMethod(i)}", SomeMethod() ne sera pas invoquée lorsque i est impair, ce qui peut ne pas être le cas être évident uniquement en inspectant l'appel à Foo()
- Pour minimiser les allocations, envisagez d'implémenter ISpanFormattable sur vos propres types s'ils peuvent être utilisés dans des chaînes interpolées (divers types de BLC implémentent cella).
Have fun!
Adriano
No comments:
Post a Comment