Expressionsbäume in C # anhand eines Beispiels zum Auffinden eines Derivats (Expression Tree Visitor vs Pattern Matching)

Schönen Tag. Ausdrucksbäume waren, besonders wenn sie mit dem Besuchermuster kombiniert wurden, immer ein ziemlich verwirrendes Thema. Je vielfältiger die Informationen zu diesem Thema sind, desto mehr Beispiele, desto einfacher wird es für diejenigen, die interessiert sind, etwas zu finden, das für sie klar und nützlich ist.



Der Artikel ist wie gewohnt aufgebaut - er beginnt mit dem konzeptionellen Rahmen und den Definitionen und endet mit Beispielen und Verwendungsmöglichkeiten. Inhaltsverzeichnis unten.

Grundlagen der Expression Trees
Syntax von Expression Trees Expression
Typen
Pattern Matching
Naive Besucher
Klassische Besucher

Nun, das ist das Ziel nicht eine bestimmte Lösung aufzuzwingen oder sagen , dass man besser ist als die andere. Ich schlage vor, selbst Schlussfolgerungen zu ziehen und dabei alle Nuancen in Ihrem Fall zu berücksichtigen. Ich werde meine Meinung zu meinem Beispiel äußern.


Ausdrucksbäume


Die Grundlagen


Zuerst müssen Sie sich mit Ausdrucksbäumen befassen. Sie bedeuten die Art des Ausdrucks oder einen seiner Erben (sie werden später besprochen). Im üblichen Szenario werden die Ausdrücke / Algorithmen in Form von ausführbarem Code / ausführbaren Anweisungen dargestellt, mit denen der Benutzer möglicherweise nicht viel zu tun hat (hauptsächlich ausführen). Mit dem Ausdruckstyp können Sie einen Ausdruck / Algorithmus (normalerweise Lambdas, aber nicht unbedingt) als Daten darstellen, die in einer Baumstruktur organisiert sind, auf die der Benutzer Zugriff hat. Die baumartige Art, Informationen über den Algorithmus und den Namen der Klasse zu organisieren, gibt uns "Ausdrucksbäume".

Zur Verdeutlichung werden wir ein einfaches Beispiel analysieren. Angenommen, wir haben Lambda

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

Dies kann als der folgende Baum dargestellt werden


Die Wurzel des Baums ist die Spitze des MethodCall . Die Parameter der Methode sind ebenfalls Ausdrücke, daher kann sie eine beliebige Anzahl von untergeordneten Elementen haben.

In unserem Fall gibt es nur einen Nachkommen - den Peak von " ArithmeticOperation ". Es enthält Informationen darüber, um welche Art von Operation es sich handelt, und der linke und der rechte Operand sind ebenfalls Ausdrücke. Ein solcher Scheitelpunkt hat immer 2 Nachkommen.

Die Operanden werden durch eine Konstante ( Konstante ) und einen Parameter ( Parameter ) dargestellt. Solche Ausdrücke haben keine Nachkommen.

Dies sind sehr vereinfachte Beispiele, die jedoch die Essenz vollständig widerspiegeln.

Das Hauptmerkmal von Ausdrucksbäumen ist, dass sie analysiert werden können und alle erforderlichen Informationen darüber lesen, was der Algorithmus tun soll. In gewisser Hinsicht ist dies das Gegenteil von Attributen. Attribute sind ein Mittel zur deklarativen Beschreibung des Verhaltens (sehr bedingt, aber das endgültige Ziel ist ungefähr dasselbe). Während Ausdrucksbäume eine Funktion / einen Algorithmus verwenden, um Daten zu beschreiben.

Sie werden beispielsweise in Entity-Framework- Anbietern verwendet . Die Anwendung liegt auf der Hand - den Ausdrucksbaum zu analysieren, zu verstehen, was dort ausgeführt werden soll, und aus dieser Beschreibung SQL zu erstellen . Weniger bekannte Beispiele sind die Moq- Bibliotheken zum Rauchen . Ausdrucksbäume werden auch im DLR verwendet.(dynamische Sprachlaufzeit). Compiler-Entwickler verwenden sie, um die Kompatibilität zwischen der dynamischen Natur und Dotnet sicherzustellen, anstatt MSIL zu generieren .

Erwähnenswert ist auch, dass Ausdrucksbäume unveränderlich sind.

Syntax


Das nächste, was es wert ist, besprochen zu werden, ist die Syntax. Es gibt zwei Hauptwege:

  • Erstellen von Ausdrucksbäumen mit statischen Methoden der Expression-Klasse
  • Verwenden von in Expression kompilierten Lambda-Ausdrücken

Statische Methoden der Ausdrucksklasse


Das Erstellen von Ausdrucksbäumen mit statischen Methoden der Ausdrucksklasse wird seltener verwendet (insbesondere aus Benutzersicht). Dies ist umständlich, aber recht einfach. Wir verfügen über
viele Grundbausteine, aus denen Sie recht komplexe
Dinge bauen können . Die Erstellung erfolgt seitdem durch statische Methoden Ausdruckskonstruktoren haben einen internen Modifikator . Und das bedeutet nicht, dass Sie Reflexion aufdecken müssen.

Als Beispiel werde ich einen Ausdruck aus dem obigen Beispiel erstellen:

(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);

Dies ist möglicherweise kein sehr bequemer Weg, spiegelt jedoch die interne Struktur von Ausdrucksbäumen vollständig wider. Darüber hinaus bietet diese Methode weitere Funktionen, die in Ausdrucksbäumen verwendet werden können: Ausgehend von Schleifen, Bedingungen, Try-Catch, Goto, Zuweisung, Beenden mit Fehlerblöcken, Debuggen von Informationen für Haltepunkte, Dynamisch usw.

Lambda-Ausdruck


Die Verwendung von Lambdas als Ausdruck ist häufiger. Es funktioniert sehr einfach - der Smart Compiler in der Kompilierungsphase untersucht, wofür Lambda verwendet wird. Und kompiliert es entweder zu einem Delegaten oder zu einem Ausdruck. In einem bereits gemasterten Beispiel sieht es wie folgt aus

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

Es lohnt sich, so etwas zu klären - ein Ausdruck ist eine erschöpfende Beschreibung. Und es reicht aus,
um das Ergebnis zu erhalten. Ausdrucksbäume wie LambdaExpression oder seine Nachkommen können
in eine ausführbare IL konvertiert werden. Die verbleibenden Typen können nicht direkt in ausführbaren Code konvertiert werden (dies ist jedoch nicht sehr sinnvoll).
Übrigens, wenn jemand die schnelle Kompilierung eines Ausdrucks kritisiert, können Sie sich dieses Projekt eines Drittanbieters ansehen .

Das Gegenteil ist im allgemeinen Fall nicht der Fall. Ein Delegierter kann es nicht einfach aufgreifen und sich als Ausdruck vorstellen (aber das ist immer noch möglich).

Nicht alle Lambdas können in Ausdrucksbäume umgewandelt werden. Diese beinhalten:

  • Enthält Zuweisungsoperator
  • Dynamisch beitragen
  • Asynchron
  • Mit Körper (Zahnspange)

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


Arten von Ausdrücken


Ich schlage einen kurzen Blick auf die verfügbaren Typen vor, um darzustellen, welche Möglichkeiten wir haben. Alle befinden sich im Namespace System.Linq.Expressions .

Ich schlage vor, dass Sie sich zuerst mit einigen wirklich interessanten und ungewöhnlichen Funktionen vertraut machen. Die einfacheren Arten von Ausdrücken habe ich in einem Tablet mit einer kurzen Beschreibung zusammengestellt.

Dynamisch


Mit DynamicExpression können Sie Dynamic und alle seine Funktionen in Ausdrucksbäumen verwenden. Es gibt eine ziemlich verwirrende API, ich habe länger an diesem Beispiel gesessen als an allen anderen zusammen. Die ganze Verwirrung wird durch eine Reihe verschiedener Flaggen verursacht. Und einige von ihnen ähneln denen, die Sie suchen, aber sie sind nicht unbedingt. Und wenn Sie mit dynamischen Ausdrucksbäumen arbeiten, ist es schwierig, einen Sprechfehler zu erhalten. Beispiel:

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

Ich habe ausdrücklich angegeben, woher der Binder stammt, um Verwechslungen mit dem Binder von System.Reflection zu vermeiden. Von interessanten Dingen können wir ref- und out-Parameter, benannte Parameter, unäre Operationen und im Prinzip alles tun, was durch Dynamik möglich ist, aber dies erfordert einige Fähigkeiten.

Ausnahmefangblöcke


Das zweite, worauf ich achten werde, ist die Funktion try / catch / finally / error oder vielmehr die Tatsache,
dass wir Zugriff auf den Fehlerblock haben. Es ist nicht in C # verfügbar, aber in MSIL. Dies ist eine Art
endgültiges Analogon, das im Falle einer Ausnahme ausgeführt wird. Im folgenden Beispiel wird eine Ausnahme ausgelöst, wonach "Hi" angezeigt wird und das Programm auf die Eingabe wartet. Erst danach wird es vollständig fallen. Ich empfehle diese Praxis nicht zur Verwendung.

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();

Kurze Beschreibung der verfügbaren Ausdrucksbaumtypen

Tabelle


Eine ArtKurzbeschreibung
Die Haupt
Ausdruck, . ,
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)


Diese Informationen reichen aus, um die Methoden zum Arbeiten mit Ausdrucksbäumen zu vergleichen. Ich beschloss, all dies am Beispiel der Suche nach dem Derivat zu analysieren. Ich habe nicht alle möglichen Optionen vorausgesehen - nur grundlegende. Aber wenn sich aus irgendeinem Grund jemand dazu entschlossen hat, es zu ändern und zu verwenden, werde ich die Verbesserungen gerne über die Anfrage an mein Repository weitergeben .


Mustervergleich


Die Aufgabe besteht also darin, eine Ableitung zu berechnen. Sie können Folgendes schätzen: Es gibt einige Regeln zum Finden der Ableitung für verschiedene Arten von Operationen - Multiplikation, Division usw. Abhängig von der Operation müssen Sie eine bestimmte Formel auswählen. In einer solchen banalen Formulierung ist die Aufgabe idealerweise auf Schalter / Fall gelegt . Und in der neuesten Version der Sprache wurde uns Switch / Case 2.0 oder Pattern Matching vorgestellt .

Es ist schwer, hier etwas zu besprechen. Auf einem Hub sieht eine solche Menge an Code umständlich und schlecht gelesen aus, daher empfehle ich, sich Github anzuschauen . Für ein Beispiel eines Derivats stellte sich Folgendes heraus:

Beispiel
    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")
             }
        };
    }


Es sieht etwas ungewöhnlich aus, aber interessant. Es war eine Freude, dies zu schreiben - alle Bedingungen passen organisch in eine Zeile.

Das Beispiel spricht für sich selbst. Sie können es nicht besser mit Worten beschreiben.

Naiver Besucher


Bei einer solchen Aufgabe fällt einem sofort ein Besucher des Ausdrucksbaums
ein, der unter den Fans viel Lärm und ein wenig Panik macht, um über Agilität in der Küche zu diskutieren. „Fürchte dich nicht vor Unwissenheit, sondern vor falschem Wissen. Es ist besser, nichts zu wissen, als die Wahrheit als das zu betrachten, was nicht wahr ist. " Wenn Sie sich an diesen wunderbaren Satz von Tolstoi erinnern, Unwissenheit anerkennen und die Unterstützung von Google in Anspruch nehmen, finden Sie die folgende Anleitung .

Ich habe diesen Link ist der erste (nach Sibirien im Jahr 1949) für die Abfrage "Ausdrucksbaum Besucher".
Genau das brauchen wir auf den ersten Blick. Der Titel des Artikels passt zu dem, was wir tun möchten, und die Klassen in den Beispielen werden mit dem Suffix Visitor benannt .

Nachdem wir den Artikel gelesen und analog zu unserem Beispiel mit Derivaten erstellt haben, erhalten wir:
Link zu Github.

Beispiel
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,
            };
    }


Tatsächlich verteilen wir die Switch-Fälle auf verschiedene Klassen. Es gab nicht weniger von ihnen, Magie erschien nicht. Trotzdem viel mehr Zeilen. Und wo ist der versprochene Doppelversand?

Klassischer Besucher und Doppelversand


Hier lohnt es sich, etwas über die Besuchervorlage selbst zu erzählen. Es ist auch Besucher , der die Grundlage für den Besucher des Ausdrucksbaums bildet . Lassen Sie es uns nur am Beispiel von Ausdrucksbäumen analysieren.

Nehmen wir für eine Sekunde an, wir entwerfen Ausdrucksbäume. Wir möchten Benutzern die Möglichkeit geben, den Ausdrucksbaum zu durchlaufen und abhängig von den Knotentypen (Ausdruckstypen) bestimmte Aktionen auszuführen.

Die erste Möglichkeit ist, nichts zu tun. Das heißt, Benutzer zwingen, Schalter / Fall zu verwenden. Dies ist keine so schlechte Option. Aber hier gibt es eine solche Nuance: Wir verbreiten die Logik, die für einen bestimmten Typ verantwortlich ist. Einfach ausgedrückt, Polymorphismus und virtuelle Herausforderungen (auch bekannt als Late Binding)) ermöglichen es, die Typdefinition auf die Laufzeit zu verschieben und diese Prüfungen aus unserem Code zu entfernen. Es reicht für uns, eine Logik zu haben, die eine Instanz des gewünschten Typs erstellt, dann wird alles zur Laufzeit für uns erledigt.

Die zweite Option.Die offensichtliche Lösung besteht darin, die Logik in virtuelle Methoden zu ziehen. Durch Überschreiben der virtuellen Methode in jedem Nachfolger können wir Switch / Case vergessen. Der Mechanismus polymorpher Aufrufe wird für uns entscheiden. Die Methodentabelle funktioniert hier, Methoden werden durch den darin enthaltenen Offset aufgerufen. Aber dies ist ein Thema für einen ganzen Artikel, also lassen wir uns nicht mitreißen. Virtuelle Methoden scheinen unser Problem zu lösen. Aber leider schaffen sie eine andere. Für unsere Aufgabe könnten wir die GetDeriviative () -Methode hinzufügen. Aber jetzt sehen die Ausdrucksklassen selbst komisch aus. Wir könnten solche Methoden für alle Gelegenheiten hinzufügen, aber sie passen nicht zur allgemeinen Logik der Klasse. Und wir haben immer noch nicht die Möglichkeit geboten, etwas Ähnliches wie Benutzer zu tun (natürlich in angemessener Weise). Wir müssen den Benutzer die Logik für jeden bestimmten Typ definieren lassen.aber behalte den Polymorphismus (der uns zur Verfügung steht).

Nur die Bemühungen des Benutzers, dies zu tun, werden nicht erfolgreich sein.

Hier liegt der wahre Besucher. In der Grundart der Hierarchie (in unserem Fall Ausdruck) definieren wir eine Methode der Form

virtual Expression Accept(ExpressionVisitor visitor);

Bei den Erben wird diese Methode überschrieben.

ExpressionVisitor selbst ist eine Basisklasse, die
für jeden Hierarchietyp eine virtuelle Methode mit derselben Signatur enthält . Am Beispiel der ExpressionVisitor-Klasse - VisitBinary (...), VisitMethodCall (...), VisitConstant (...), VisitParameter (...).

Diese Methoden werden in der entsprechenden Klasse unserer Hierarchie aufgerufen.

Jene. Die Accept-Methode in der BinaryExpression-Klasse sieht folgendermaßen aus:

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

Um ein neues Verhalten zu definieren, muss der Benutzer daher nur einen Erben der ExpressionVisitor-Klasse erstellen, in dem die entsprechenden Methoden zur Lösung eines Problems neu definiert werden. In unserem Fall wird ein DerivativeExpressionVisitor erstellt.

Weiter haben wir einige Objekte der Nachfolger von Expression, aber welche sind unbekannt, aber nicht notwendig.
Wir rufen die virtuelle Accept-Methode mit der ExpressionVisitor-Implementierung auf, die wir benötigen, d. H. mit DerivativeExpressionVisitor. Dank des dynamischen Dispatchings wird eine überschriebene Implementierung von Accept aufgerufen, z. B. eine Laufzeit, z. B. BinaryExpression. Im Verlauf dieser Methode verstehen wir genau, dass wir uns in BinaryExpression befinden, wissen jedoch nicht, welcher ExpressionVisitor-Nachfolger zu uns gekommen ist. Aber seit VisitBinary ist auch virtuell, wir müssen es nicht wissen. Wieder rufen wir einfach den Verweis auf die Basisklasse auf, der Aufruf wird dynamisch (zur Laufzeit) ausgelöst und eine überschriebene VisitBinary-Implementierung des Laufzeittyps wird aufgerufen. Soviel zum doppelten Versand - Tischtennis im Stil von „du machst es“ - „nein, du“.

Was gibt es uns? Auf diese Weise können virtuelle Methoden von außen hinzugefügt werden, nicht
die Klasse ändern. Es klingt großartig, hat aber auch Nachteile:

  1. Einige Linke in Form der Accept-Methode, die gleichzeitig für alles und für nichts verantwortlich ist
  2. Der Welligkeitseffekt einer guten Hash-Funktion besteht darin, dass im schlimmsten Fall jeder seine Besucher beenden muss, wenn Sie der Hierarchie nur einen Erben hinzufügen

Die Art der Ausdrucksbäume ermöglicht diese Kosten jedoch aufgrund der Besonderheiten der Arbeit mit Ausdrücken, da diese Art von Problemumgehungen eines ihrer Hauptmerkmale ist.
Hier sehen Sie alle verfügbaren Methoden zum Überladen.

Mal sehen, wie es am Ende aussieht.
Link zum Github.

Beispiel
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;
    }


Ergebnisse


Vielleicht kann, wie bei den meisten Programmieraufgaben, keine eindeutige Antwort gegeben werden. Alles hängt wie immer von der jeweiligen Situation ab. Ich mag den üblichen Mustervergleich für mein Beispiel, weil Ich habe es nicht im Maßstab der industriellen Entwicklung entwickelt. Wenn dieser Ausdruck unkontrolliert zunehmen würde, wäre es wert, über den Besucher nachzudenken. Und selbst ein naiver Besucher hat das Recht auf Leben - schließlich ist dies eine gute Möglichkeit, eine große Menge Code in Klassen zu verteilen, wenn die Hierarchie ihrerseits keine Unterstützung geleistet hat. Und auch hier gibt es Ausnahmen.

Ebenso ist die Unterstützung des Besuchers durch die Hierarchie sehr kontrovers.

Ich hoffe jedoch, dass die hier bereitgestellten Informationen ausreichen, um die richtige Wahl zu treffen.

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


All Articles