Dec 28, 2022

Une petit casse-têtes C# 10 - La solution (Part III)

Leia este post em português

Read this post in English.

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).

J'ai également dit que si vous regardez attentivement dans le code, vous pourrez trouver des indices sur la direction de la solution basée sur des morceaux de code qui n'étaient pas essentiels dans cette version du code qui contient essentiellement 2 de ces morceaux de code :

  1. 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.

  2. 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:

Si vous exécutez le programme ci-dessus, vous verrez qu'il fonctionne effectivement, c'est-à-dire que vous ne devriez voir que les lignes avec des nombres pairs dans la sortie. Avant de plonger dans les détails, examinons ce qui est nécessaire pour implémenter des interpolated string handler personnalisé :
  1. Déclarez un type et ajoutez-y l'InterpolatedStringHandlerAttribute (ligne #14)

  2. 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)

  3. Mettre en œuvre les méthodes 
    1. void AppendLiteral(string msg)
    2. void AppendFormatted<T>(valeur T)
et c'est tout.

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 :

Le code réel sera un peu différent (essentiellement, il convertira la boucle for en un while et introduira des variables locales supplémentaires) mais j'ai décidé de l'ignorer et de le garder aussi proche que possible de l'original, ce qui simplifie le raisonnement.

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 :
  1. 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.

  2. É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).

  3. Étant que String.Format() n'est plus utilisé, aucun array n'est alloué.

  4. 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)

  5. 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()

  6. 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).
Enfin, si vous souhaitez en savoir plus sur cette fonctionnalité, je vous recommande fortement de regarder cette vidéo et de lire ce tutoriel MS.

Have fun!

Adriano

No comments: