Partager via


Arborescences d’expressions - données qui définissent le code

Une arborescence d’expressions est une structure de données qui définit le code. Les arborescences d’expressions sont basées sur les mêmes structures qu’un compilateur utilise pour analyser le code et générer la sortie compilée. Lorsque vous lisez cet article, vous remarquez un peu de similarité entre les arborescences d’expressions et les types utilisés dans les API Roslyn pour générer des analyseurs et des codeFixes. (Les analyseurs et les codeFixes sont des packages NuGet qui effectuent une analyse statique sur le code et suggèrent des correctifs potentiels pour un développeur.) Les concepts sont similaires et le résultat final est une structure de données qui permet d’examiner le code source de manière significative. Toutefois, les arborescences d’expressions sont basées sur un ensemble différent de classes et d’API que les API Roslyn. Voici une ligne de code :

var sum = 1 + 2;

Si vous analysez le code précédent en tant qu’arborescence d’expressions, l’arborescence contient plusieurs nœuds. Le nœud le plus externe est une instruction de déclaration de variable avec affectation (var sum = 1 + 2;) Ce nœud externe contient plusieurs nœuds enfants : une déclaration de variable, un opérateur d’affectation et une expression représentant le côté droit du signe égal. Cette expression est ensuite subdivisée en expressions qui représentent l’opération d’ajout, ainsi que les opérandes gauche et droit de l’ajout.

Penchons-nous un peu plus sur les expressions qui composent la partie à droite du signe égal. L’expression est 1 + 2, une expression binaire. Plus précisément, il s’agit d’une expression d’ajout binaire. Une expression d’ajout binaire a deux enfants, représentant les nœuds gauche et droit de l’expression d’ajout. Ici, les deux nœuds sont des expressions constantes : l’opérande gauche est la valeur 1, et l’opérande droit est la valeur 2.

Visuellement, l’instruction entière est une arborescence : vous pouvez commencer au nœud racine et passer à chaque nœud de l’arborescence pour voir le code qui compose l’instruction :

  • Instruction de déclaration de variable avec affectation (var sum = 1 + 2;)
    • Déclaration de type de variable implicite (var sum)
      • Mot clé var implicite (var)
      • Déclaration de nom de variable (sum)
    • Opérateur d’assignation (=)
    • Expression d’ajout binaire (1 + 2)
      • Opérande gauche (1)
      • Opérateur d’addition (+)
      • Opérande droit (2)

L’arbre précédent peut sembler compliqué, mais il est très puissant. Après le même processus, vous décomposez des expressions beaucoup plus complexes. Considérez cette expression :

var finalAnswer = this.SecretSauceFunction(
    currentState.createInterimResult(), currentState.createSecondValue(1, 2),
    decisionServer.considerFinalOptions("hello")) +
    MoreSecretSauce('A', DateTime.Now, true);

L’expression précédente est également une déclaration de variable avec une affectation. La partie droite de l’assignation est une arborescence beaucoup plus complexe. Vous n’allez pas décomposer cette expression, mais considérez ce que peuvent être les différents nœuds. Il existe des appels de méthode utilisant l’objet actuel en tant que récepteur : l’un possède un récepteur explicite this, l’autre n’en possède pas. Il existe des appels de méthode utilisant d’autres objets récepteurs, il existe des arguments constants de différents types. Enfin, il existe un opérateur d’ajout binaire. Selon le type de retour de SecretSauceFunction() ou MoreSecretSauce(), cet opérateur d’ajout binaire peut être un appel de méthode à un opérateur d’ajout substitué, ce qui se traduit par un appel de méthode statique à l’opérateur d’ajout binaire défini pour une classe.

Malgré cette complexité perçue, l’expression précédente crée une arborescence qui navigue aussi facilement que le premier échantillon. Vous traversez les nœuds enfants successifs pour rechercher des nœuds terminaux dans l’expression. Les nœuds parents ont des références à leurs enfants, et chaque nœud a une propriété qui décrit le type de nœud qu’il est.

La structure d’une arborescence d’expressions est très cohérente. Une fois que vous avez appris les principes de base, vous comprenez même le code le plus complexe lorsqu’il est représenté en tant qu’arborescence d’expressions. L’élégance de la structure de données explique comment le compilateur C# analyse les programmes C# les plus complexes et crée une sortie appropriée à partir de ce code source compliqué.

Une fois que vous êtes familiarisé avec la structure des arborescences d’expressions, vous constatez que les connaissances que vous avez acquises vous permettent rapidement de travailler avec de nombreux scénarios plus avancés. Les arborescences d’expressions offrent une puissance incroyable.

Outre la traduction d’algorithmes à exécuter dans d’autres environnements, les arborescences d’expressions facilitent l’écriture d’algorithmes qui inspectent le code avant de l’exécuter. Vous écrivez une méthode dont les arguments sont des expressions, puis examinez ces expressions avant d’exécuter le code. L’arborescence d’expressions est une représentation complète du code : vous voyez les valeurs d’une sous-expression. Vous voyez les noms des méthodes et des propriétés. Vous voyez la valeur de toutes les expressions constantes. Vous convertissez une arborescence d’expressions en délégué exécutable et exécutez le code.

Les API pour les arborescences d’expressions vous permettent de créer des arborescences qui représentent presque toutes les constructions de code valides. Toutefois, pour garder les choses aussi simples que possible, certains idiomes C# ne peuvent pas être créés dans une arborescence d’expressions. Voici un exemple d’expressions asynchrones (en utilisant les mots clés async et await). Si vos besoins nécessitent des algorithmes asynchrones, vous devez manipuler les Task objets directement, plutôt que de vous appuyer sur la prise en charge du compilateur. Une autre consiste à créer des boucles. En règle générale, vous créez ces boucles à l’aide de boucles for, foreach, while ou do. Comme vous le voyez plus loin dans cette série, les API pour les arborescences d’expressions prennent en charge une expression de boucle unique, avec break et continue des expressions qui contrôlent la répétition de la boucle.

La seule chose que vous ne pouvez pas faire est de modifier une arborescence d’expressions. Les arborescences d’expressions sont des structures de données immuables. Si vous souhaitez muter (modifier) une arborescence d’expressions, vous devez créer une arborescence qui est une copie de l’original, mais avec vos modifications souhaitées.