Arbres d'expression en C # à l'aide d'un exemple de recherche d'un dérivé (Expression Tree Visitor vs Pattern matching)

Bonne journée. Les arbres d'expression, en particulier lorsqu'ils sont combinés avec le modèle Visitor, ont toujours été un sujet assez déroutant. Par conséquent, plus l'information sur ce sujet est diversifiée, plus il y a d'exemples, plus il sera facile pour ceux qui sont intéressés de trouver quelque chose de clair et utile pour eux.



L'article est construit comme d'habitude - il commence par le cadre conceptuel et les définitions et se termine par des exemples et des modes d'utilisation. Table des matières ci-dessous.

Notions de base des arborescences d'expression
Syntaxe des arborescences d'expression
Types d' expressions
Correspondance du
visiteur naïf Visiteur
classique

Eh bien, le but n'est pas d'imposer une solution spécifique ou de dire que l'une est meilleure que l'autre. Je propose de tirer nous-mêmes des conclusions en tenant compte de toutes les nuances de votre cas. Je vais exprimer mon opinion sur mon exemple.


Arbres d'expression


Les bases


Vous devez d'abord gérer les arbres d'expression. Ils signifient le type d'expression ou l'un de ses héritiers (ils seront discutés plus tard). Dans le scénario habituel, l'expression / les algorithmes sont présentés sous la forme de code / d'instructions exécutables avec lesquels l'utilisateur peut ne pas avoir grand-chose à faire (principalement exécuter). Le type Expression vous permet de représenter une expression / un algorithme (généralement des lambdas, mais pas nécessaire) sous forme de données organisées dans une arborescence à laquelle l'utilisateur a accès. La façon arborescente d'organiser les informations sur l'algorithme et le nom de la classe nous donne des "arbres d'expression".

Pour plus de clarté, nous analyserons un exemple simple. Supposons que nous ayons lambda

(x) => Console.WriteLine (x + 5)

Cela peut être représenté comme l'arborescence suivante


La racine de l'arborescence est le sommet de MethodCall , les paramètres de la méthode sont également des expressions, elle peut donc avoir n'importe quel nombre d'enfants.

Dans notre cas, il n'y a qu'un seul descendant - le sommet de " ArithmeticOperation ". Il contient des informations sur le type d'opération et les opérandes gauche et droite sont également des expressions. Un tel sommet aura toujours 2 descendants.

Les opérandes sont représentés par une constante ( constante ) et un paramètre ( paramètre ). De telles expressions n'ont pas de descendants.

Ce sont des exemples très simplifiés, mais reflètent pleinement l'essence.

La principale caractéristique des arbres d'expression est qu'ils peuvent être analysés et lire toutes les informations nécessaires sur ce que l'algorithme doit faire. D'un certain point de vue, c'est l'opposé des attributs. Les attributs sont un moyen de description déclarative du comportement (très conditionnel, mais le but ultime est à peu près le même). Tandis que les arbres d'expression utilisent une fonction / un algorithme pour décrire les données.

Ils sont utilisés, par exemple, dans les fournisseurs de structure d'entité. L'application est évidente - pour analyser l'arbre d'expressions, comprendre ce qui doit y être exécuté et faire du SQL à partir de cette description . Moins d' exemples bien connus sont la moq bibliothèque moqueur . Les arbres d'expression sont également utilisés dans le DLR.(runtime de langage dynamique). Les développeurs de compilateurs les utilisent pour assurer la compatibilité entre la nature dynamique et dotnet, au lieu de générer MSIL .

Il convient également de mentionner que les arbres d'expression sont immuables.

Syntaxe


La prochaine chose à discuter est la syntaxe. Il existe 2 façons principales:

  • Création d'arbres d'expression via des méthodes statiques de la classe Expression
  • Utilisation d'expressions lambda compilées dans Expression

Méthodes statiques de la classe d'expression


La création d'arbres d'expression à l'aide de méthodes statiques de la classe Expression est moins couramment utilisée (en particulier du point de vue de l'utilisateur). C'est lourd, mais assez simple, nous avons
beaucoup de briques de base à notre disposition, à partir desquelles vous pouvez construire des
choses assez complexes . La création se fait par des méthodes statiques depuis les constructeurs d'expression ont un modificateur interne . Et cela ne signifie pas que vous devez découvrir la réflexion.

À titre d'exemple, je vais créer une expression à partir de l'exemple ci-dessus:

(x) => Console.WriteLine (x + 5)

ParameterExpression parameter = Expression.Parameter(typeof(double));
ConstantExpression constant = Expression.Constant(5d, typeof(double));
BinaryExpression add = Expression.Add(parameter, constant);
MethodInfo writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(double) });
MethodCallExpression methodCall = Expression.Call(null, writeLine, add);
Expression<Action<double>> expressionlambda = Expression.Lambda<Action<double>>(methodCall, parameter);
Action<double> delegateLambda = expressionlambda.Compile();
delegateLambda(123321);

Ce n'est peut-être pas un moyen très pratique, mais cela reflète pleinement la structure interne des arbres d'expression. De plus, cette méthode fournit plus de fonctionnalités et de fonctionnalités qui peuvent être utilisées dans les arborescences d'expressions: à partir de boucles, de conditions, de try-catch, de goto, d'affectation, se terminant par des blocs de défaut, des informations de débogage pour les points d'arrêt, dynamiques, etc.

Expression lambda


L'utilisation de lambdas comme expressions est une manière plus fréquente. Cela fonctionne très simplement - le compilateur intelligent au stade de la compilation examine à quoi sert lambda. Et le compile soit en délégué, soit en expression. Sur un exemple déjà maîtrisé, il se présente comme suit

Expression<Action<double>> write =  => Console.WriteLine( + 5);

Il vaut la peine de clarifier une telle chose - une expression est une description exhaustive. Et cela suffit pour
obtenir le résultat. Les arbres d'expression tels que LambdaExpression ou ses descendants peuvent être
convertis en IL exécutable. Les types restants ne peuvent pas être convertis directement en code exécutable (mais cela n'a pas beaucoup de sens).
Soit dit en passant, si quelqu'un critique la compilation rapide d'une expression, vous pouvez jeter un œil à ce projet tiers.

L'inverse n'est pas vrai dans le cas général. Un délégué ne peut pas simplement le ramasser et se présenter comme une expression (mais cela est toujours possible).

Tous les lambdas ne peuvent pas être convertis en arbres d'expression. Ceux-ci inclus:

  • Contenant l'opérateur d'affectation
  • Dynamique contributive
  • Asynchrone
  • Avec corps (bretelles)

double variable;
dynamic dynamic;
Expression<Action> assignment = () => variable = 5; //Compiler error: An expression tree may not contain an assignment operator
Expression<Func<double>> dynamically = () => dynamic; //Compiler error: An expression tree may not contain a dynamic operation
Expression<Func<Task>> asynchon = async () => await Task.CompletedTask; //Compiler error: Async lambda cannot be converted to expresiion trees
Expression<Action> body = () => { }; //Compiler error: A lambda expression with a statement body cannot be converted to an expression tree


Types d'expressions


Je suggère un aperçu rapide des types disponibles pour représenter les opportunités que nous avons. Tous se trouvent dans l' espace de noms System.Linq.Expressions .

Je vous suggère de vous familiariser avec plusieurs fonctionnalités vraiment intéressantes et inhabituelles. Les types d'expressions les plus simples que j'ai rassemblés dans une tablette avec une brève description.

Dynamique


En utilisant DynamicExpression, il est possible d'utiliser dynamique et toutes ses fonctionnalités dans les arborescences d'expression. Il y a une API assez déroutante, je me suis assis sur cet exemple plus longtemps que sur tous les autres combinés. Toute la confusion est fournie par un tas de drapeaux différents. Et certains d'entre eux sont similaires à ceux que vous recherchez, mais ils ne le sont pas nécessairement. Et lorsque vous travaillez avec des arbres d'expression dynamiques, il est difficile d'obtenir une erreur de conversation. Exemple:

var parameter1 = Expression.Parameter(typeof(object), "name1");
var parameter2 = Expression.Parameter(typeof(object), "name2"); 
var dynamicParam1 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
var dynamicParam2 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
CallSiteBinder csb = Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Add, typeof(Program), new[] { dynamicParam1, dynamicParam2 });
var dyno = Expression.Dynamic(csb, typeof(object), parameter1, parameter2);
Expression<Func<dynamic, dynamic, dynamic>> expr = Expression.Lambda<Func<dynamic, dynamic, dynamic>>(dyno, new[] { parameter1, parameter2 });
Func<dynamic, dynamic, dynamic> action = expr.Compile();
var res = action("1", "2");
Console.WriteLine(res); //12
res = action(1, 2);
Console.WriteLine(res); //3

J'ai explicitement indiqué d'où vient le classeur afin d'éviter toute confusion avec le classeur de System.Reflection. Parmi les choses intéressantes, nous pouvons faire des paramètres ref et out, des paramètres nommés, des opérations unaires et, en principe, tout ce qui peut être fait via la dynamique, mais cela nécessitera une certaine compétence.

Blocs de capture d'exception


La deuxième chose à laquelle je ferai attention est la fonctionnalité try / catch / finalement / fault, ou plutôt le fait
que nous ayons accès au bloc de défaut. Il n'est pas disponible en C #, mais il est en MSIL C'est une sorte de
finalement analogique qui sera exécuté en cas d'exception. Dans l'exemple ci-dessous, une exception sera levée, après quoi "Hi" sera affiché et le programme attendra l'entrée. Ce n'est qu'après cela qu'il tombera complètement. Je ne recommande pas cette pratique pour une utilisation.

var throwSmth = Expression.Throw(Expression.Constant(new Exception(), typeof(Exception)));
var log = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }), Expression.Constant("Hi", typeof(string)));
var read = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.ReadLine)));
var fault = Expression.TryFault(throwSmth, Expression.Block(new[] { log, read }));
Expression<Action> expr = Expression.Lambda<Action>(fault);
Action compiledExpression = expr.Compile();
compiledExpression();

Brève description des types d'arbres d'expression disponibles

Table


Un typeBrève description
Le principal
Expression, . ,
Expression<TDelegate>
BinaryExpression(+, — )
UnaryExpression(+, -), throw
ConstantExpression
ParameterExpression
MethodCallExpression, MethodInfo
IndexExpression
BlockExpression, .
ConditionalExpression— if-else
LabelTargetgoto
LabelExpression, . LabelTarget. , GotoExpression, — . void, .
GotoExpression. . ( .. «break»)
LoopExpression, «break»
SwitchCaseSwitchExpression
SwitchExpressionswitch/case
TryExpressiontry/catch/finally/fault
CatchBlock, ,
ElementInitIEnumerable. ListInitExpression
ListInitExpression+
DefaultExpression
NewArrayExpression+
NewExpression
/
MemberAssignment
MemberBinding, , ,
MemberExpression/
MemberInitExpression
MemberListBinding/
MemberMemberBinding/ , /
LambdaExpression
InvocationExpression-
DebugInfoExpression.
SymbolDocumentInfo, .
DynamicExpression( )
RuntimeVariablesExpression/
TypeBinaryExpression, (is)



  • ExpressionVisitor — . .
  • DynamicExpressionVisitor — DynamicExpression ( VisitDynamic)


Ces informations sont suffisantes pour commencer à comparer les méthodes de travail avec les arbres d'expression. J'ai décidé d'analyser tout cela par l'exemple de trouver le dérivé. Je n'ai pas prévu toutes les options possibles - seulement celles de base. Mais si pour une raison quelconque quelqu'un a décidé de le modifier et de l'utiliser, je serai heureux de partager les améliorations via la demande à mon référentiel .


Correspondance de motifs


Donc, la tâche est de faire un calcul des dérivés. Vous pouvez estimer ce qui suit: il existe quelques règles pour trouver la dérivée pour différents types d'opérations - multiplication, division, etc. Selon l'opération, vous devez sélectionner une formule spécifique. Dans une formulation aussi banale, la tâche est idéalement placée sur le commutateur / boîtier . Et dans la dernière version du langage, nous avons été présentés avec switch / case 2.0 ou pattern matching .

Il est difficile de discuter de quelque chose ici. Sur un hub, une telle quantité de code semble lourde et mal lue, je suggère donc de regarder github . Pour un exemple d'un dérivé, il s'est avéré comme ceci:

Exemple
    public class PatterntMatchingDerivative
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));
		
        public Expression<Func<double, double>> ParseDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(ParseDerivative(function.Body), function.Parameters);
        }

        private Expression ParseDerivative(Expression function) => function switch
        {
            BinaryExpression binaryExpr => function.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            },
            MethodCallExpression methodCall when methodCall.Method == _pow => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(ParseDerivative(expression), methodCall), Expression.Call(null, _log, constant)),
             },
             _ => function.NodeType switch
            {
                ExpressionType.Constant => _zero,
                ExpressionType.Parameter => _one,
                _ => throw new OutOfMemoryException("Bitmap best practice")
             }
        };
    }


Cela semble un peu inhabituel, mais intéressant. Ce fut un plaisir d'écrire ceci - toutes les conditions tiennent organiquement sur une seule ligne.

L'exemple parle de lui-même, vous ne pouvez pas le décrire mieux avec des mots.

Visiteur naïf


Dans une telle tâche, un visiteur de l'arbre d'expression vient immédiatement à l'esprit, ce qui fait beaucoup de bruit
et un peu de panique parmi les fans pour discuter de l'agilité dans la cuisine. «Ne craignez pas l'ignorance, mais la fausse connaissance. Il vaut mieux ne rien savoir que de considérer la vérité comme ce qui n'est pas vrai. " Rappelant cette merveilleuse phrase de Tolstoï, reconnaissant l'ignorance et obtenant le soutien de Google, vous pouvez trouver le guide suivant .

J'ai ce lien est le premier (après la Sibérie en 1949) pour la requête "visiteur de l'arbre d'expression".
À première vue, c'est exactement ce dont nous avons besoin. Le titre de l'article correspond à ce que nous voulons faire, et les classes dans les exemples sont nommées avec le suffixe Visitor .

Après avoir revu l'article et réalisé, par analogie avec notre exemple avec les dérivés, on obtient:
Lien vers github.

Exemple
public class CustomDerivativeExpressionTreeVisitor
    {
        public Expression<Func<double, double>> Visit(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visitor.CreateFromExpression(function.Body).Visit(), function.Parameters);
        }
    }

    public abstract class Visitor
    {
        protected static readonly MethodInfo Pow = typeof(Math).GetMethod(nameof(Math.Pow));
        protected static readonly MethodInfo Log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        protected readonly ConstantExpression Zero = Expression.Constant(0d, typeof(double));
        protected readonly ConstantExpression One = Expression.Constant(1d, typeof(double));
        public abstract Expression Visit();
        public static Visitor CreateFromExpression(Expression node)
            => node switch
            {
                BinaryExpression be => new BinaryVisitor(be),
                MethodCallExpression mce when mce.Method == Pow => new PowMethodCallVisitor(mce),
                _ => new SimpleVisitor(node),
            };
        
    }

    public class BinaryVisitor : Visitor
    {
        private readonly BinaryExpression _node;
        
        public BinaryVisitor(BinaryExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            };
    }

    public class PowMethodCallVisitor : Visitor
    {
        private readonly MethodCallExpression _node;

        public PowMethodCallVisitor(MethodCallExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => (_node.Arguments[0], _node.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(_node, Expression.Call(null, Log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, Pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(CreateFromExpression(expression).Visit(), _node), Expression.Call(null, Log, constant)),
            };
    }

    public class SimpleVisitor : Visitor
    {
        private readonly Expression _node;

        public SimpleVisitor(Expression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Constant => Zero,
                ExpressionType.Parameter => One,
            };
    }


En fait - nous répartissons les cas de commutation pour différentes classes. Il n'y en avait pas moins, la magie n'apparaissait pas. Tous les mêmes cas, beaucoup plus de lignes. Et où est la promesse de double expédition promise?

Visiteur classique et double expédition


Ici, il convient de parler du modèle de visiteur lui - même , il s'agit également de Visitor , qui est la base du visiteur de l'arbre d'expression . Analysons-le uniquement sur l'exemple des arbres d'expression.

Supposons un instant que nous concevions des arbres d'expression. Nous voulons donner aux utilisateurs la possibilité d'itérer dans l'arborescence d'expressions et, selon les types de nœuds (types d'expression), effectuer certaines actions.

La première option est de ne rien faire. Autrement dit, forcer les utilisateurs à utiliser un commutateur / boîtier. Ce n'est pas une si mauvaise option. Mais ici, il y a une telle nuance: nous diffusons la logique responsable d'un type particulier. Autrement dit, polymorphisme et défis virtuels ( aka liaison tardive) permettent de déplacer la définition de type vers le runtime et de supprimer ces contrôles de notre code. Il nous suffit d'avoir une logique qui crée une instance du type souhaité, alors tout sera fait par le runtime pour nous.

La deuxième option.La solution évidente consiste à intégrer la logique dans des méthodes virtuelles. En remplaçant la méthode virtuelle dans chaque successeur, nous pouvons oublier switch / case. Le mécanisme des appels polymorphes décidera pour nous. La table des méthodes fonctionnera ici, les méthodes seront appelées par le décalage qui s'y trouve. Mais c'est un sujet pour un article entier, alors ne nous laissons pas emporter. Les méthodes virtuelles semblent résoudre notre problème. Mais malheureusement, ils en créent un autre. Pour notre tâche, nous pourrions ajouter la méthode GetDeriviative (). Mais maintenant, les classes d'expression elles-mêmes semblent étranges. Nous pourrions ajouter de telles méthodes pour toutes les occasions, mais elles ne correspondent pas à la logique générale de la classe. Et nous n'avons toujours pas donné l'occasion de faire quelque chose de similaire aux utilisateurs (de manière adéquate, bien sûr). Nous devons laisser l'utilisateur définir la logique de chaque type particulier,mais gardez le polymorphisme (qui est à notre disposition).

Seuls les efforts de l'utilisateur pour y parvenir ne réussiront pas.

C'est là que réside le vrai visiteur. Dans le type de hiérarchie de base (Expression dans notre cas), nous définissons une méthode de la forme

virtual Expression Accept(ExpressionVisitor visitor);

Chez les héritiers, cette méthode sera remplacée.

ExpressionVisitor lui-même est une classe de base qui contient une méthode virtuelle avec la même signature
pour chaque type de hiérarchie. En utilisant la classe ExpressionVisitor comme exemple, VisitBinary (...), VisitMethodCall (...), VisitConstant (...), VisitParameter (...).

Ces méthodes sont appelées dans la classe correspondante de notre hiérarchie.

Ceux. La méthode Accept dans la classe BinaryExpression ressemblera à ceci:

	
protected internal override Expression Accept(ExpressionVisitor visitor)
{
        return visitor.VisitBinary(this);
}

Par conséquent, pour définir un nouveau comportement, l'utilisateur n'a qu'à créer un héritier de la classe ExpressionVisitor, dans laquelle les méthodes correspondantes pour résoudre un problème seront redéfinies. Dans notre cas, un DerivativeExpressionVisitor est créé.

De plus, nous avons certains objets des successeurs d'Expression, mais lesquels sont inconnus, mais pas nécessaires.
Nous appelons la méthode virtuelle Accept avec l'implémentation ExpressionVisitor dont nous avons besoin, c'est-à-dire avec DerivativeExpressionVisitor. Grâce à la répartition dynamique, une implémentation remplacée d'Accept est appelée, comme un run-time, par exemple BinaryExpression. Dans le corps de cette méthode, nous comprenons parfaitement que nous sommes dans BinaryExpression, mais nous ne savons pas quel successeur d'ExpressionVisitor est venu nous voir. Mais depuis VisitBinary est également virtuel, nous n'avons pas besoin de le savoir. Encore une fois, nous appelons simplement la référence à la classe de base, l'appel est envoyé dynamiquement (au moment de l'exécution) et une implémentation VisitBinary substituée du type d'exécution est appelée. Voilà pour la double dépêche - ping-pong dans le style «tu le fais» - «non, toi».

Qu'est-ce que cela nous donne. En fait, cela permet d’ajouter des méthodes virtuelles de l’extérieur,
changer la classe. Cela semble génial, mais a ses inconvénients:

  1. Certains gauchistes sous la forme de la méthode Accept, qui est responsable de tout et de rien en même temps
  2. L'effet d'entraînement d'une bonne fonction de hachage est que lorsque vous ajoutez un seul héritier à la hiérarchie, dans le pire des cas, tout le monde devra finir ses visiteurs

Mais la nature des arbres d'expression permet ces coûts en raison des spécificités du travail avec les expressions, car ce type de solutions de contournement est l'une de leurs principales caractéristiques.
Ici vous pouvez voir toutes les méthodes disponibles pour la surcharge.

Voyons donc à quoi ça ressemble à la fin.
Lien vers github.

Exemple
public class BuildinExpressionTreeVisitor : ExpressionVisitor
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));

        public Expression<Func<double, double>> GetDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visit(function.Body), function.Parameters);
        }

        protected override Expression VisitBinary(BinaryExpression binaryExpr)
            => binaryExpr.NodeType switch
            {
                ExpressionType.Add => Expression.Add(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),

                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => binaryExpr.Left,
                ExpressionType.Multiply when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => binaryExpr.Right,
                ExpressionType.Multiply => Expression.Add(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))),

                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => Expression.Divide(binaryExpr.Left, Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
                ExpressionType.Divide when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => Expression.Divide(_one, binaryExpr.Right),
                ExpressionType.Divide => Expression.Divide(Expression.Subtract(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
            };

        protected override Expression VisitMethodCall(MethodCallExpression methodCall)
            => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(Visit(expression), methodCall), Expression.Call(null, _log, constant)),
            };

        protected override Expression VisitConstant(ConstantExpression _) => _zero;

        protected override Expression VisitParameter(ParameterExpression b) => _one;
    }


résultats


Peut-être, comme dans la plupart des tâches de programmation, une réponse définitive ne peut être donnée. Tout, comme toujours, dépend de la situation spécifique. J'aime la correspondance de motifs habituelle pour mon exemple, car Je ne l'ai pas développé à l'échelle du développement industriel. Si cette expression augmentait de façon incontrôlable, cela valait la peine de penser au visiteur. Et même un visiteur naïf a droit à la vie - après tout, c'est un bon moyen de disperser une grande quantité de code dans des classes si la hiérarchie n'a pas fourni de support de sa part. Et même ici, il y a des exceptions.

De même, le soutien du visiteur de la hiérarchie est une chose très controversée.

Mais j'espère que les informations fournies ici sont suffisantes pour faire le bon choix.

Source: https://habr.com/ru/post/undefined/


All Articles