Funktionale C#-Programmierung im Detail (7) Ausdrucksbaum:Funktion als Daten

Funktionale C#-Programmierung im Detail (7) Ausdrucksbaum:Funktion als Daten

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-function-as-data-and-expression-tree

Der C#-Lambda-Ausdruck ist ein mächtiger syntaktischer Zucker. Neben der Darstellung einer anonymen Funktion kann dieselbe Syntax auch einen Ausdrucksbaum darstellen.

Lambda-Ausdruck als Ausdrucksbaum

Eine Ausdrucksbaumstruktur kann mit derselben Lambda-Ausdruckssyntax für anonyme Funktionen erstellt werden:

internal static partial class ExpressionTree
{
    internal static void ExpressionLambda()
    {
        // Func<int, bool> isPositive = int32 => int32 > 0;
        Expression<Func<int, bool>> isPositiveExpression = int32 => int32 > 0;
    }
}

Dieses Mal ist der erwartete Typ für den Lambda-Ausdruck nicht mehr der Funktionstyp Func, sondern Expression>. Der Lambda-Ausdruck wird hier nicht mehr zu einer ausführbaren anonymen Funktion kompiliert, sondern zu einer Baumdatenstruktur, die die Logik dieser Funktion darstellt, die als Ausdrucksbaum bezeichnet wird.

Metaprogrammierung:Funktion als Daten

Der obige Lambda-Ausdruck wird in den Ausdrucksbaum-Erstellungscode kompiliert:

internal static void CompiledExpressionLambda()
{
    ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "int32"); // int32 parameter.
    ConstantExpression constantExpression = Expression.Constant(0, typeof(int)); // 0
    BinaryExpression greaterThanExpression = Expression.GreaterThan(
        left: parameterExpression, right: constantExpression); // int32 > 0

    Expression<Func<int, bool>> isPositiveExpression = Expression.Lambda<Func<int, bool>>(
        body: greaterThanExpression, // ... => int32 > 0
        parameters: parameterExpression); // int32 => ...
}

Hier stellt die Expression>-Instanz den gesamten Baum dar, die ParameterExpression-, ConstantExpression- und BinaryExpression-Instanzen sind Knoten in diesem Baum. Und sie sind alle von System.Linq.Expressions.Expression type:

abgeleitet
namespace System.Linq.Expressions
{
    public abstract partial class Expression
    {
        public virtual ExpressionType NodeType { get; }

        public virtual Type Type { get; }

        // Other members.
    }

    public class ParameterExpression : Expression
    {
        public string Name { get; }

        // Other members.
    }

    public class ConstantExpression : Expression
    {
        public object Value { get; }

        // Other members.
    }

    public class BinaryExpression : Expression
    {
        public Expression Left { get; }

        public Expression Right { get; }

        // Other members.
    }

    public abstract class LambdaExpression : Expression
    {
        public Expression Body { get; }

        public ReadOnlyCollection<ParameterExpression> Parameters { get; }

        // Other members.
    }

    public sealed class Expression<TDelegate> : LambdaExpression
    {
        public TDelegate Compile();

        // Other members.
    }
}

Die obige Ausdrucksbaum-Datenstruktur kann visualisiert werden als:

Expression<Func<int, bool>> (NodeType = Lambda, Type = Func<int, bool>)
|_Parameters
| |_ParameterExpression (NodeType = Parameter, Type = int)
|   |_Name = "int32"
|_Body
  |_BinaryExpression (NodeType = GreaterThan, Type = bool)
    |_Left
    | |_ParameterExpression (NodeType = Parameter, Type = int)
    |   |_Name = "int32"
    |_Right
      |_ConstantExpression (NodeType = Constant, Type = int)
        |_Value = 0

Dieser Ausdrucksbaum ist also ein abstrakter syntaktischer Baum, der die abstrakte syntaktische Struktur des C#-Funktionsquellcodes int32 => int32> 0 darstellt. Beachten Sie, dass jeder Knoten über die NodeType-Eigenschaft und die Type-Eigenschaft verfügt. NodeType gibt den dargestellten Konstrukttyp in der Struktur zurück, und Type gibt den dargestellten .NET-Typ zurück. Beispielsweise ist ParameterExpression oben ein Parameterknoten, der einen int-Parameter im Quellcode darstellt, also ist sein NodeType Parameter und sein Type ist int.

Zusammenfassend die Unterschiede zwischen

Func<int, bool> isPositive = int32 => int32 > 0; // Code.

und

Expression<Func<int, bool>> isPositiveExpression = int32 => int32 > 0; // Data.

sind:

  • Die isPositive-Variable ist eine Funktion, die durch eine Delegate-Instanz repräsentiert wird und aufgerufen werden kann. Der Lambda-Ausdruck int32 => int32> 0 wird zu ausführbarem Code kompiliert. Wenn isPositive aufgerufen wird, wird dieser Code ausgeführt.
  • Die isPositiveExpression-Variable ist eine abstrakte syntaktische Baumdatenstruktur. Anscheinend kann es also nicht direkt wie eine ausführbare Funktion aufgerufen werden. Der Lambda-Ausdruck int32 => int32> 0 wird zum Aufbau einer Ausdrucksbaumstruktur kompiliert, wobei jeder Knoten eine Ausdrucksinstanz ist. Dieser gesamte Baum stellt die syntaktische Struktur und Logik der Funktion int32 => int32> 0 dar. Der oberste Knoten dieses Baums ist eine Expression>-Instanz, da dies ein Lambda-Ausdruck ist. Es hat 2 untergeordnete Knoten:
    • Eine ParameterExpression-Sammlung, die alle Parameter des Lambda-Ausdrucks darstellt. Der Lambda-Ausdruck hat 1 Parameter, also enthält diese Sammlung einen Knoten:
      • Eine ParameterExpression-Instanz, die den int-Parameter namens „int32“ darstellt.
    • Ein Body-Knoten, der den Body des Lambda-Ausdrucks darstellt, der eine BinaryExpression-Instanz ist, die den Body darstellt, ist ein „>“-Vergleich (größer als) von 2 Operanden. Es hat also 2 untergeordnete Knoten:
      • Eine Referenz der obigen ParameterExpression-Instanz, die den linken Operanden darstellt.
      • Eine ConstantExpression-Instanz, die den rechten Operanden 0 darstellt.

Weil jeder Knoten in der Ausdrucksstruktur mit umfangreichen Informationen stark typisiert ist. Die Knoten können durchlaufen werden, um die C#-Quellcodelogik der dargestellten Funktion zu erhalten und in die Logik einer anderen Sprache zu konvertieren. Hier stellt isPositiveExpression die Funktionslogik dar, die prädiziert, ob ein int-Wert größer als eine Konstante 0 ist, und kann in einer SQL-WHERE-Klausel usw. in das Größer-als-Prädikat einer SQL-Abfrage konvertiert werden.

.NET-Ausdrücke

Neben ParameterExpression, ConstantExpression, BinaryExpression, LambdaExpression bietet .NET eine umfangreiche Sammlung von Ausdrucksknoten. Das Folgende ist ihre Vererbungshierarchie:

  • Ausdruck
    • BinärerAusdruck
    • Blockausdruck
    • BedingterAusdruck
    • KonstanterAusdruck
    • DebugInfoExpression
    • Standardausdruck
    • Dynamischer Ausdruck
    • GotoExpression
    • Indexausdruck
    • Aufrufausdruck
    • Etikettenausdruck
    • Lambda-Ausdruck
      • Ausdruck
    • ListInitExpression
    • Schleifenausdruck
    • Mitgliedsausdruck
    • MemberInitExpression
    • MethodCallExpression
    • Neuer Array-Ausdruck
    • NeuerAusdruck
    • Parameterausdruck
    • Laufzeitvariablenausdruck
    • Wechselausdruck
    • Ausdruck versuchen
    • TypeBinaryExpression
    • UnaryExpression

Und wie oben demonstriert, kann der Ausdruck durch Aufrufen der Factory-Methoden des Ausdruckstyps instanziiert werden:

public abstract partial class Expression
{
    public static ParameterExpression Parameter(Type type, string name);

    public static ConstantExpression Constant(object value, Type type);

    public static BinaryExpression GreaterThan(Expression left, Expression right);

    public static Expression<TDelegate> Lambda<TDelegate>(Expression body, params ParameterExpression[] parameters);
}

Expression verfügt über viele andere Factory-Methoden, um alle Fälle der Ausdrucksinstanziierung abzudecken:

public abstract partial class Expression
{
    public static BinaryExpression Add(Expression left, Expression right);

    public static BinaryExpression Subtract(Expression left, Expression right);

    public static BinaryExpression Multiply(Expression left, Expression right);

    public static BinaryExpression Divide(Expression left, Expression right);

    public static BinaryExpression Equal(Expression left, Expression right);

    public static UnaryExpression ArrayLength(Expression array);

    public static UnaryExpression Not(Expression expression);

    public static ConditionalExpression Condition(Expression test, Expression ifTrue, Expression ifFalse);

    public static NewExpression New(ConstructorInfo constructor, params Expression[] arguments);

    public static MethodCallExpression Call(MethodInfo method, params Expression[] arguments);

    public static BlockExpression Block(params Expression[] expressions);

    // Other members.
}

Einige Ausdrucksknoten können mehrere mögliche NodeType-Werte haben. Zum Beispiel:

  • UnaryExpression repräsentiert jede unäre Operation mit einem Operator und einem Operanden. Sein NodeType kann ArrayLength, Negate, Not, Convert, Decreament, Increment, Throw, UnaryPlus usw. sein.
  • BinaryExpression repräsentiert jede binäre Operation mit einem Operator, einem linken Operanden und einem rechten Operanden, sein NodeType kann Add, And, Assign, Divide, Equal, .GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Modulo, Multiply, NotEqual sein, Oder, Potenz, Subtrahieren usw.

Bisher implementiert der C#-Compiler diesen syntaktischen Zucker „Funktion als Daten“ nur für den Lambda-Ausdruck, und er ist noch nicht für die Anweisung Lambda verfügbar. Der folgende Code kann nicht kompiliert werden:

internal static void StatementLambda()
{
    Expression<Func<int, bool>> isPositiveExpression = int32 =>
    {
        Console.WriteLine(int32);
        return int32 > 0;
    };
}

Dies führt zu einem Compilerfehler:Ein Lambda-Ausdruck mit einem Anweisungstext kann nicht in eine Ausdrucksbaumstruktur konvertiert werden. Der obige Ausdrucksbaum muss manuell erstellt werden:

internal static void StatementLambda()
{
    ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "int32"); // int32 parameter.
    Expression<Func<int, bool>> isPositiveExpression = Expression.Lambda<Func<int, bool>>(
        body: Expression.Block( // ... => {
            // Console.WriteLine(int32);
            Expression.Call(new Action<int>(Console.WriteLine).Method, parameterExpression),
            // return int32 > 0;
            Expression.GreaterThan(parameterExpression, Expression.Constant(0, typeof(int)))), // }
        parameters: parameterExpression); // int32 => ...
}

Ausdrucksbaum in CIL kompilieren

Ausdrucksbaum ist Daten - abstrakter syntaktischer Baum. In C# und LINQ wird der Ausdrucksbaum normalerweise verwendet, um die abstrakte syntaktische Struktur der Funktion darzustellen, damit sie in andere domänenspezifische Sprachen wie SQL-Abfrage, URI-Abfrage usw. kompiliert werden kann. Um dies zu demonstrieren, nehmen Sie eine einfache mathematische Funktion als Beispiel, das doppelte Parameter akzeptiert und die 4 grundlegenden binären arithmetischen Berechnungen ausführt:addieren, subtrahieren, multiplizieren, dividieren:

internal static void ArithmeticalExpression()
{
    Expression<Func<double, double, double, double, double, double>> expression =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
}

Der gesamte Baum kann wie folgt dargestellt werden:

Expression<Func<double, double, double, double, double, double>> (NodeType = Lambda, Type = Func<double, double, double, double, double, double>)
|_Parameters
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "a"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "b"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "c"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "d"
| |_ParameterExpression (NodeType = Parameter, Type = double)
|   |_Name = "e"
|_Body
  |_BinaryExpression (NodeType = Add, Type = double)
    |_Left
    | |_BinaryExpression (NodeType = Subtract, Type = double)
    |   |_Left
    |   | |_BinaryExpression (NodeType = Add, Type = double)
    |   |   |_Left
    |   |   | |_ParameterExpression (NodeType = Parameter, Type = double)
    |   |   |   |_Name = "a"
    |   |   |_Right
    |   |     |_ParameterExpression (NodeType = Parameter, Type = double)
    |   |       |_Name = "b"
    |   |_Right
    |     |_BinaryExpression (NodeType = Divide, Type = double)
    |       |_Left
    |       | |_BinaryExpression (NodeType = Multiply, Type = double)
    |       |   |_Left
    |       |   | |_ParameterExpression (NodeType = Parameter, Type = double)
    |       |   |   |_Name = "c"
    |       |   |_right
    |       |     |_ParameterExpression (NodeType = Parameter, Type = double)
    |       |       |_Name = "d"
    |       |_Right
    |         |_ConstantExpression (NodeType = Constant, Type = int)
    |           |_Value = 2
    |_Right
      |_BinaryExpression (NodeType = Multiply, Type = double)
        |_Left
        | |_ParameterExpression (NodeType = Parameter, Type = double)
        |   |_Name = "e"
        |_Right
          |_ConstantExpression (NodeType = Constant, Type = int)
            |_Value = 3

Dies ist ein sehr einfacher Ausdrucksbaum, wobei:

  • jeder interne Knoten ist ein binärer Knoten (BinaryExpression-Instanz), der binäre Operationen zum Addieren, Subtrahieren, Multiplizieren oder Dividieren darstellt;
  • Jeder Blattknoten ist entweder ein Parameter (ParameterExpression-Instanz) oder eine Konstante (ConstantExpression-Instanz).

Insgesamt gibt es in diesem Baum 6 Arten von Knoten:

  • hinzufügen:BinaryExpression { NodeType =ExpressionType.Add }
  • subtrahieren:BinaryExpression { NodeType =ExpressionType.Subtract }
  • multiplizieren:BinaryExpression { NodeType =ExpressionType.Multiply }
  • Teilen:BinaryExpression { NodeType =ExpressionType.Divide}
  • Konstante:ParameterExpression { NodeType =ExpressionType.Constant }
  • Parameter:ConstantExpression { NodeType =ExpressionType.Parameter }

Traverse-Ausdrucksbaum

Das rekursive Durchlaufen dieses Baums ist sehr einfach. Der folgende Basistyp implementiert die Grundlogik des Durchlaufens:

internal abstract class BinaryArithmeticExpressionVisitor<TResult>
{
    internal virtual TResult VisitBody(LambdaExpression expression) => this.VisitNode(expression.Body, expression);

    protected TResult VisitNode(Expression node, LambdaExpression expression)
    {
        // Processes the 6 types of node.
        switch (node.NodeType)
        {
            case ExpressionType.Add:
                return this.VisitAdd((BinaryExpression)node, expression);

            case ExpressionType.Constant:
                return this.VisitConstant((ConstantExpression)node, expression);

            case ExpressionType.Divide:
                return this.VisitDivide((BinaryExpression)node, expression);

            case ExpressionType.Multiply:
                return this.VisitMultiply((BinaryExpression)node, expression);

            case ExpressionType.Parameter:
                return this.VisitParameter((ParameterExpression)node, expression);

            case ExpressionType.Subtract:
                return this.VisitSubtract((BinaryExpression)node, expression);

            default:
                throw new ArgumentOutOfRangeException(nameof(node));
        }
    }

    protected abstract TResult VisitAdd(BinaryExpression add, LambdaExpression expression);

    protected abstract TResult VisitConstant(ConstantExpression constant, LambdaExpression expression);

    protected abstract TResult VisitDivide(BinaryExpression divide, LambdaExpression expression);

    protected abstract TResult VisitMultiply(BinaryExpression multiply, LambdaExpression expression);

    protected abstract TResult VisitParameter(ParameterExpression parameter, LambdaExpression expression);

    protected abstract TResult VisitSubtract(BinaryExpression subtract, LambdaExpression expression);
}

Die VisitNode-Methode erkennt den Knotentyp und sendet an 6 abstrakte Methoden für alle 6 Arten von Knoten. Der folgende Typ implementiert diese 6 Methoden:

internal class PrefixVisitor : BinaryArithmeticExpressionVisitor<string>
{
    protected override string VisitAdd
        (BinaryExpression add, LambdaExpression expression) => this.VisitBinary(add, "add", expression);

    protected override string VisitConstant
        (ConstantExpression constant, LambdaExpression expression) => constant.Value.ToString();

    protected override string VisitDivide
        (BinaryExpression divide, LambdaExpression expression) => this.VisitBinary(divide, "div", expression);

    protected override string VisitMultiply
        (BinaryExpression multiply, LambdaExpression expression) =>
            this.VisitBinary(multiply, "mul", expression);

    protected override string VisitParameter
        (ParameterExpression parameter, LambdaExpression expression) => parameter.Name;

    protected override string VisitSubtract
        (BinaryExpression subtract, LambdaExpression expression) =>
            this.VisitBinary(subtract, "sub", expression);

    private string VisitBinary( // Recursion: operator(left, right)
        BinaryExpression binary, string @operator, LambdaExpression expression) =>
            $"{@operator}({this.VisitNode(binary.Left, expression)}, {this.VisitNode(binary.Right, expression)})";
}

Beim Besuch eines Binärknotens gibt er rekursiv im Präfixstil Operator (links, rechts) aus. Beispielsweise wird der Infix-Ausdruck a + b in add(a, b) konvertiert, was als Aufruf der add-Funktion mit den Argumenten a und b angesehen werden kann. Der folgende Code gibt die Logik des Funktionskörpers im Funktionsaufrufstil mit Präfix aus:

internal static partial class ExpressionTree
{
    internal static void Prefix()
    {
        Expression<Func<double, double, double, double, double, double>> infix =
            (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
        PrefixVisitor prefixVisitor = new PrefixVisitor();
        string prefix = prefixVisitor.VisitBody(infix); // add(sub(add(a, b), div(mul(c, d), 2)), mul(e, 3))
    }
}

Tatsächlich stellt .NET einen integrierten System.Linq.Expressions.ExpressionVisitor-Typ bereit. Hier sind Traverser nur zu Demonstrationszwecken von Grund auf neu implementiert.

Ausdrucksbaum zu CIL zur Laufzeit

Wenn die Ausgabe im Postfix-Stil ist (a, b, add), dann kann sie wie folgt angesehen werden:a in den Stack laden, b in den Stack laden, 2 Werte auf dem Stack hinzufügen. So funktioniert die Stack-basierte CIL-Sprache. So kann ein anderer Besucher erstellt werden, um CIL-Anweisungen auszugeben. CIL-Anweisungen können durch System.Reflection.Emit.OpCode-Strukturen dargestellt werden. Die Ausgabe kann also eine Folge von Anweisungs-Argument-Paaren sein, dargestellt durch ein Tupel aus einem OpCode-Wert und einem Double-Wert (Operand) oder Null (kein Operand):

internal class PostfixVisitor : BinaryArithmeticExpressionVisitor<List<(OpCode, double?)>>
{
    protected override List<(OpCode, double?)> VisitAdd(
        BinaryExpression add, LambdaExpression expression) => this.VisitBinary(add, OpCodes.Add, expression);

    protected override List<(OpCode, double?)> VisitConstant(
        ConstantExpression constant, LambdaExpression expression) =>
            new List<(OpCode, double?)>() { (OpCodes.Ldc_R8, (double?)constant.Value) };

    protected override List<(OpCode, double?)> VisitDivide(
        BinaryExpression divide, LambdaExpression expression) =>
            this.VisitBinary(divide, OpCodes.Div, expression);

    protected override List<(OpCode, double?)> VisitMultiply(
        BinaryExpression multiply, LambdaExpression expression) =>
            this.VisitBinary(multiply, OpCodes.Mul, expression);

    protected override List<(OpCode, double?)> VisitParameter(
        ParameterExpression parameter, LambdaExpression expression)
    {
        int index = expression.Parameters.IndexOf(parameter);
        return new List<(OpCode, double?)>() { (OpCodes.Ldarg_S, (double?)index) };
    }

    protected override List<(OpCode, double?)> VisitSubtract(
        BinaryExpression subtract, LambdaExpression expression) =>
            this.VisitBinary(subtract, OpCodes.Sub, expression);

    private List<(OpCode, double?)> VisitBinary( // Recursion: left, right, operator
        BinaryExpression binary, OpCode postfix, LambdaExpression expression)
    {
        List<(OpCode, double?)> cils = this.VisitNode(binary.Left, expression);
        cils.AddRange(this.VisitNode(binary.Right, expression));
        cils.Add((postfix, (double?)null));
        return cils;
    }
}

Der folgende Code gibt eine Folge von CIL-Code aus:

internal static void Cil()
{
    Expression<Func<double, double, double, double, double, double>> infix =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;

    PostfixVisitor postfixVisitor = new PostfixVisitor();
    IEnumerable<(OpCode, double?)> postfix = postfixVisitor.VisitBody(infix);
    foreach ((OpCode Operator, double? Operand) code in postfix)
    {
        $"{code.Operator} {code.Operand}".WriteLine();
    }
    // ldarg.s 0
    // ldarg.s 1
    // add
    // ldarg.s 2
    // ldarg.s 3 
    // mul 
    // ldc.r8 2 
    // div 
    // sub 
    // ldarg.s 4 
    // ldc.r8 3 
    // mul 
    // add
}

Die in dieser Ausdrucksbaumstruktur dargestellte C#-Logik wird also erfolgreich in die CIL-Sprache kompiliert.

Zur Laufzeit funktionierender Ausdrucksbaum

Der oben kompilierte CIL-Code ist ausführbar, sodass eine Funktion zur Laufzeit erstellt werden kann, dann kann der CIL-Code in diese Funktion emittiert werden. Diese Art von Funktion wird als dynamische Funktion bezeichnet, da sie sich nicht in einer statischen Assembly befindet, die zur Kompilierzeit generiert wird, sondern zur Laufzeit generiert wird.

internal static class BinaryArithmeticCompiler
{
    internal static TDelegate Compile<TDelegate>(Expression<TDelegate> expression)
    {
        DynamicMethod dynamicFunction = new DynamicMethod(
            name: string.Empty,
            returnType: expression.ReturnType,
            parameterTypes: expression.Parameters.Select(parameter => parameter.Type).ToArray(),
            m: typeof(BinaryArithmeticCompiler).Module);
        EmitIL(dynamicFunction.GetILGenerator(), new PostfixVisitor().VisitBody(expression));
        return (TDelegate)(object)dynamicFunction.CreateDelegate(typeof(TDelegate));
    }

    private static void EmitIL(ILGenerator ilGenerator, IEnumerable<(OpCode, double?)> il)
    {
        foreach ((OpCode Operation, double? Operand) code in il)
        {
            if (code.Operand == null)
            {
                ilGenerator.Emit(code.Operation); // add, sub, mul, div
            }
            else if (code.Operation == OpCodes.Ldarg_S)
            {
                ilGenerator.Emit(code.Operation, (int)code.Operand); // ldarg.s (int)index
            }
            else
            {
                ilGenerator.Emit(code.Operation, code.Operand.Value); // ldc.r8 (double)constant
            }
        }
        ilGenerator.Emit(OpCodes.Ret); // Returns the result.
    }
}

Der folgende Code demonstriert seine Verwendung:

internal static void Compile()
{
    Expression<Func<double, double, double, double, double, double>> expression =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
    Func<double, double, double, double, double, double> function = 
        BinaryArithmeticCompiler.Compile(expression);
    double result = function(1, 2, 3, 4, 5); // 12
}

.NET bietet zu diesem Zweck eine integrierte API, die Compile-Methode von System.Linq.Expressions.Expression – Kompilieren Sie die Ausdrucksbaumstruktur zur Laufzeit in eine ausführbare Funktion:

internal static void BuiltInCompile()
{
    Expression<Func<double, double, double, double, double, double>> infix =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
    Func<double, double, double, double, double, double> function = infix.Compile();
    double result = function(1, 2, 3, 4, 5); // 12
}

Intern ruft Expression.Compile APIs von System.Linq.Expressions.Compiler.LambdaCompile auf, das eine vollständige Ausdrucksbaumstruktur für die Implementierung des CIL-Compilers ist.

Ausdrucksbaum und LINQ-Remote-Abfrage

Die Ausdrucksbaumstruktur ist in LINQ-Remoteabfragen sehr wichtig, da es einfach ist, eine Ausdrucksbaumstruktur zu erstellen, insbesondere mit dem Lambda-Ausdruck, und es auch einfach ist, die Logik einer C#-Ausdrucksbaumstruktur in eine andere Domäne oder andere Sprache zu kompilieren/konvertieren/übersetzen. In den obigen Beispielen wird der Ausdrucksbaum in eine ausführbare CIL konvertiert. Wie bereits erwähnt, gibt es lokale und entfernte LINQ-Abfragen, wie z. B. relationale Datenbanken. Die folgenden Beispiele sind eine lokale LINQ to Objects-Abfrage für lokale Objekte im Arbeitsspeicher und eine Remote-LINQ to Entities-Abfrage für relationale Datenbanken:

internal static partial class ExpressionTree
{
    internal static void LinqToObjects(IEnumerable<Product> source)
    {
        IEnumerable<Product> query = source.Where(product => product.ListPrice > 0M); // Define query.
        foreach (Product result in query) // Execute query.
        {
            result.Name.WriteLine();
        }
    }

    internal static void LinqToEntities(IQueryable<Product> source)
    {
        IQueryable<Product> query = source.Where(product => product.ListPrice > 0M); // Define query.
        foreach (Product result in query) // Execute query.
        {
            result.Name.WriteLine();
        }
    }
}

Die Datenquelle der obigen LINQ to Objects-Abfrage ist eine Sequenz von Produktobjekten im lokalen Speicher der aktuellen .NET-Anwendung. Die Datenquelle der LINQ to Entities-Abfrage ist die Produkttabelle in der relationalen Remotedatenbank, die im aktuellen lokalen Speicher nicht verfügbar ist. In LINQ werden lokale Datenquelle und Abfrage durch IEnumerable dargestellt, und Remotedatenquelle und -abfrage werden durch IQueryable dargestellt. Sie haben unterschiedliche LINQ-Abfrageerweiterungsmethoden, Tabelle oben, wobei als Beispiel:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source, Func<TSource, bool> predicate);
    }

    public static class Queryable
    {
        public static IQueryable<TSource> Where<TSource>(
            this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
    }
}

Infolgedessen haben die Where-Abfrage und der Prädikat-Lambda-Ausdruck dieselbe Syntax für lokale und Remote-LINQ-Abfragen, aber ihre Kompilierung ist völlig unterschiedlich. Das Prädikat der lokalen Abfrage wird zur Funktion kompiliert, und das Prädikat der Remote-Abfrage wird zur Ausdrucksbaumstruktur kompiliert:

internal static partial class CompiledExpressionTree
{
    [CompilerGenerated]
    private static Func<Product, bool> cachedPredicate;

    [CompilerGenerated]
    private static bool Predicate(Product product) => product.ListPrice > 0M;

    public static void LinqToObjects(IEnumerable<Product> source)
    {
        Func<Product, bool> predicate = cachedPredicate ?? (cachedPredicate = Predicate);
        IEnumerable<Product> query = Enumerable.Where(source, predicate);
        foreach (Product result in query) // Execute query.
        {
            TraceExtensions.WriteLine(result.Name);
        }
    }
}

internal static partial class CompiledExpressionTree
{
    internal static void LinqToEntities(IQueryable<Product> source)
    {
        ParameterExpression productParameter = Expression.Parameter(typeof(Product), "product");
        Expression<Func<Product, bool>> predicateExpression = Expression.Lambda<Func<Product, bool>>(
            Expression.GreaterThan(
                Expression.Property(productParameter, nameof(Product.ListPrice)),
                Expression.Constant(0M, typeof(decimal))),
            productParameter);

        IQueryable<Product> query = Queryable.Where(source, predicateExpression); // Define query.
        foreach (Product result in query) // Execute query.
        {
            TraceExtensions.WriteLine(result.Name);
        }
    }
}

Wenn zur Laufzeit die lokale Abfrage ausgeführt wird, wird die anonyme Funktion für jeden lokalen Wert in der Quellsequenz aufgerufen, und die Remote-Abfrage wird normalerweise in eine domänenspezifische Sprache übersetzt, dann an die Remote-Datenquelle übermittelt und ausgeführt. Hier in der LINQ to Entities-Abfrage wird die Prädikat-Ausdrucksstruktur in ein Prädikat in einer SQL-Abfrage übersetzt und zur Ausführung an die Datenbank übermittelt. Die Übersetzung vom Ausdrucksbaum nach SQL wird im Kapitel LINQ to Entities behandelt.