Freigeben über



September 2019

Ausgabe 34, Nummer 9

[.NET-Entwicklung]

Ausdrucksbaumstrukturen in Visual Basic und C#

von Zev Spitz

Stellen Sie sich folgendes Szenario vor: Sie verfügen über Objekte, die bestimmte Teile eines Programms darstellen. In Ihrem Code können diese Teile auf verschiedene Weisen kombiniert werden, um ein Objekt zu erstellen, das ein neues Programm darstellt. Dieses Objekt können Sie dann kompilieren und als neues Programm ausführen. Ihr Programm kann sogar seine eigene Logik neu schreiben. Außerdem haben Sie die Möglichkeit, dieses Objekt einer anderen Umgebung zu übergeben, in der die einzelnen Teile als Anweisungen analysiert und ausgeführt werden.

Ausdrucksbaumstrukturen bieten Ihnen genau diese Möglichkeiten. In diesem Artikel erhalten Sie einen Überblick über Ausdrucksbaumstrukturen. Sie erfahren, wie diese eingesetzt werden, um Codeoperationen zu modellieren und Code zur Laufzeit zu kompilieren. Außerdem wird beschrieben, wie diese Strukturen erstellt und transformiert werden.

Hinweise zu Programmiersprachen Die Syntax vieler Sprachfeatures in Visual Basic und C# ist im Grunde genommen nicht mehr als ein einfacher Wrapper für sprachunabhängige .NET-Typen und -Methoden. Beispielsweise rufen sowohl die „foreach“-Schleife in C# als auch das „For Each“-Konstrukt in Visual Basic die „GetEnumerator“-Methode des Typs auf, der „IEnumerable“ implementiert. Die LINQ-Schlüsselwortsyntax wird in Methoden wie „Select“ und „Where“ mit der entsprechenden Signatur aufgelöst. Ein Aufruf von „IDisposable.Dispose“, der in Fehlerbehandlungscode eingeschlossen ist, wird immer dann erzeugt, wenn Sie „Using“ in „Visual Basic“ oder einen „using“-Block in C# verwenden.

Dies gilt auch für Ausdrucksbaumstrukturen. Sowohl Visual Basic als auch C# unterstützen syntaktisch die Erstellung von Ausdrucksbaumstrukturen und können die .NET-Infrastruktur (im Namespace „System.Linq.Expressions“) nutzen, um mit diesen zu interagieren. Die hier beschriebenen Konzepte lassen sich daher gleichermaßen auf beide Sprachen anwenden. Insofern werden Ihnen die Informationen in diesem Artikel sicherlich einen Mehrwert bieten – ganz egal, ob Sie C# oder Visual Basic als Hauptentwicklungssprache verwenden.

Ausdrücke, Ausdrucksbaumstrukturen und Typen in „System.Linq.Expressions“

In Visual Basic und C# ist ein Ausdruck Code, der einen Wert zurückgibt, wenn er ausgewertet wird. Sehen Sie sich die folgenden Beispiele an:

42
"abcd"
True
n

Ausdrücke können auch aus anderen Ausdrücken bestehen, wie das folgende Beispiel veranschaulicht:

x + y
"abcd".Length < 5 * 2

Auf diese Weise entstehen Ausdrucksbaumstrukturen. Die Ausdrücke „n + 42 = 27“ in Visual Basic oder „n + 42 == 27“ in C# können in mehrere Teile gegliedert werden (siehe Abbildung 1).

Aufschlüsselung einer Ausdrucksbaumstruktur
Abbildung 1: Aufschlüsselung einer Ausdrucksbaumstruktur

.NET stellt mehrere Typen im Namespace „System.Linq.Expressions“ zur Erstellung von Datenstrukturen bereit, die Ausdrucksbaumstrukturen darstellen. Der Ausdruck „n + 42 = 27“ kann beispielsweise wie auf Abbildung 2 gezeigt mit Objekten und Eigenschaften dargestellt werden. Beachten Sie, dass sich dieser Code nicht kompilieren lässt, da die Typen nicht über öffentliche Konstruktoren verfügen und unveränderlich sind. Daher sind auch keine Objektinitialisierer verfügbar.

Abbildung 2: Ausdrucksbaumstruktur-Objekte, für die die Objektnotation verwendet wird

New BinaryExpression With {
  .NodeType = ExpressionType.Equal,
  .Left = New BinaryExpression With {
    .NodeType = ExpressionType.Add,
    .Left = New ParameterExpression With {
      .Name = "n"
    },
    .Right = New ConstantExpression With {
      .Value = 42
    }
  },
  .Right = New ConstantExpression With {
    .Value = 27
  }
}
new BinaryExpression {
  NodeType = ExpressionType.Equal,
  Left = new BinaryExpression {
    NodeType = ExpressionType.Add,
    Left = new ParameterExpression {
      Name = "n"
    },
    Right = new ConstantExpression {
      Value = 42
    }
  },
  Right = new ConstantExpression {
    Value = 27
  }
}

In .NET bezeichnet „Ausdrucksbaumstruktur“ sowohl syntaktische Ausdrucksbaumstrukturen (x + y) als auch Ausdrucksbaumstruktur-Objekte (beispielsweise eine „Binary­Expression“-Instanz).

Der Typ eines Knotenobjekts – also „BinaryExpression“, „ConstantExpression“ oder „ParameterExpression“ – beschreibt nur die Form des Ausdrucks, um zu kennzeichnen, was ein bestimmter Knoten in der Ausdrucksbaumstruktur darstellt. Der Typ „BinaryExpression“ gibt z. B. Aufschluss darüber, dass der dargestellte Ausdruck über zwei Operanden verfügt. Er lässt jedoch nicht erkennen, durch welche Operation diese verknüpft werden. Daher ist unklar, ob die Operanden addiert, multipliziert oder numerisch verglichen werden. Diese Informationen sind in der „NodeType“-Eigenschaft enthalten, die in der „Expression“-Basisklasse definiert ist. Beachten Sie, dass einige Knotentypen keine Codeoperationen darstellen. Weitere Informationen dazu finden Sie unter „Metaknotentypen“.

Metaknotentypen

Die meisten Knotentypen stellen Codeoperationen dar. Es gibt jedoch auch drei Metaknotentypen. Diese stellen Informationen über die Struktur bereit, werden jedoch nicht direkt dem Code zugeordnet.

ExpressionType.Quote Dieser Knotentyp umschließt immer ein „LambdaExpression“-Objekt und legt fest, dass dieses eine neue Ausdrucksbaumstruktur und keinen Delegaten definiert. Die aus dem folgenden Visual Basic-Code erzeugte Struktur

Dim expr As Expression(Of Func(Of Func(Of Boolean))) = Function() Function() True

oder der folgende C#-Code

Expression<Func<Func<bool>>> expr = () => () => true;

stellen beispielsweise einen Delegaten dar, der einen weiteren Delegaten erzeugt. Wenn Sie einen Delegaten darstellen möchten, der eine weitere Ausdrucksbaumstruktur erzeugt, müssen Sie den inneren Delegaten mit einem „Quote“-Knotenobjekt umschließen. Der Compiler führt diesen Vorgang automatisch durch den folgenden Visual Basic-Code aus:

Dim expr As Expression(Of Func(Of Expression(Of Func(Of Boolean)))) =
  Function() Function() True

Alternativ kann auch der folgende C#-Code verwendet werden:

 

Expression<Func<Expression<Func<bool>>>> expr = () => () => true;

ExpressionType.DebugInfo Dieser Knoten gibt Debuginformationen aus. Wenn Sie kompilierte Ausdrücke debuggen, kann die IL zu einem bestimmten Zeitpunkt der richten Stelle im Quellcode zugeordnet werden.

ExpressionType.RuntimeVariablesExpression In ECMAScript 3 ist beispielsweise das „arguments“-Objekt auch ohne explizite Deklaration als Variable innerhalb einer Funktion verfügbar. In Python steht die „locals“-Funktion zur Verfügung, die ein Wörterbuch mit Variablen zurückgibt, die im lokalen Namespace definiert sind. Diese „virtuellen“ Variablen werden in einer Ausdrucksbaumstruktur mithilfe eines „RuntimeVariablesExpression“-Objekts beschrieben.

Eine zusätzliche erwähnenswerte Eigenschaft für „Expression“ ist die „Type“-Eigenschaft. In Visual Basic und C# verfügt jeder Ausdruck über einen Typ. Aus der Addition zweier ganzer Zahlen folgt der Integer-Datentyp, aus der Addition zweier Gleitkommazahlen mit doppelter Genauigkeit der Double-Datentyp. Die „Type“-Eigenschaft gibt ein „System.Type“-Objekt für den Ausdruck zurück.

Bei der Visualisierung einer Ausdrucksbaumstruktur ist es sinnvoll, sich auf diese beiden Eigenschaften zu konzentrieren (siehe Abbildung 3).

die Eigenschaften „NodeType“, „Type“, „Value“ und „Name“
Abbildung 3: die Eigenschaften „NodeType“, „Type“, „Value“ und „Name“

Wie können diese Typen erstellt werden, wenn für sie keine öffentlichen Konstruktoren verfügbar sind? Sie haben zwei Möglichkeiten. Die erste besteht darin, dass der Compiler die Typen für Sie erstellt, wenn Sie die Lambdasyntax an Stellen verwenden, an denen ein Ausdruckstyp erwartet wird. Die zweite besteht darin, dass Sie für die „Expression“-Klasse Factorymethoden mit den Schlüsselwörtern „Shared“ oder in C# „static“ verwenden.

Erstellen von Ausdrucksbaumstruktur-Objekten (I): Verwenden des Compilers

Wie bereits erwähnt, besteht die einfachste Möglichkeit zur Erzeugung der Objekte darin, den Compiler diese mithilfe der Lambdasyntax generieren zu lassen. Dies geschieht an allen Stellen, an denen „Expression(Of TDelegate)“ (in C# „Expression<TDelegate>“) erwartet wird. Infrage kommen dabei entweder eine Zuweisung zu einer typisierten Variablen oder ein Argument für einen typisierten Parameter. In Visual Basic wird die Lambdasyntax mit dem Schlüsselwort „Function“ oder „Sub“ eingeleitet. Es folgen eine Parameterliste und ein Methodenkörper. In C# wird hingegen der Operator => verwendet.

Wenn mit der Lambdasyntax eine Ausdrucksbaumstruktur definiert wird, spricht man von einem Ausdruckslambda (im Gegensatz zu einem Anweisungslambda), das in C# wie folgt aussieht:

Expression<Func<Integer, String>> expr = i => i.ToString();
IQueryable<Person> personSource = ...
var qry = qry.Select(x => x.LastName);

In Visual Basic wird folgende Syntax verwendet:

Dim expr As Expression(Of Func(Of Integer, String))  = Function(i) i.ToString
Dim personSource As IQueryable(Of Person) = ...
Dim qry = qry.Select(Function(x) x.LastName)

Wenn auf diese Weise eine Ausdrucksbaumstruktur erzeugt wird, müssen mehrere Einschränkungen berücksichtigt werden:

  • Je nach Lambdasyntax ergeben sich unterschiedliche Einschränkungen.
  • Nur einzeilige (nicht jedoch mehrzeilige) Lambdaausdrücke werden unterstützt.
  • Ausdruckslambdas können nur Ausdrücke und keine Anweisungen wie „If … Then“ oder „Try … Catch“ enthalten.
  • Späte (oder in C# dynamische) Bindungen können nicht genutzt werden.
  • In C# können keine benannten Argumente verwendet oder optionalen Argumente ausgelassen werden.
  • Der NULL-Conditional-Operator (auch als NULL-Propagation-Operator bezeichnet) kann nicht verwendet werden.

Die vollständige Ausdrucksbaumstruktur ergibt sich aus Knotentypen, Typen von Unterausdrücken und der Auflösung von Methodenüberladungen und wird zur Kompilierzeit festgelegt. Da Ausdrucksbaumstrukturen unveränderlich sind, können sie nach ihrer Erstellung nicht mehr angepasst werden.

Vom Compiler generierte Strukturen imitieren außerdem das Verhalten des Compilers, weswegen sich die tatsächliche Struktur möglicherweise von der unterscheidet, die angesichts des ursprünglichen Codes zu erwarten wäre (siehe Abbildung 4). Im Folgenden sehen Sie einige Beispiele:

vom Compiler generierte Strukturen und Quellcode im Vergleich
Abbildung 4: vom Compiler generierte Strukturen und Quellcode im Vergleich

In Lambdaausdrücke eingeschlossene Variablen des umgebenden Gültigkeitsbereichs Beim Aufrufen und Verlassen von Lambdaausdrücken muss auf die aktuellen Variablenwerte zugegriffen werden. Dazu erstellt der Compiler eine anonyme Klasse, deren Member den Variablen entsprechen müssen, auf die verwiesen wird. Die Funktion des Lambdaausdrucks wird zur Methode der Klasse (weitere Informationen finden Sie unter „In Lambdaausdrücke eingeschlossene Variablen des umgebenden Gültigkeitsbereichs“). Die entsprechende Ausdrucksbaumstruktur stellt Variablen, die auf diese Weise in Lambdaausdrücke eingeschlossen werden, als „MemberAccess“-Knoten für die anonyme Instanz dar. In Visual Basic wird dem Variablennamen außerdem das Präfix „$VB$Local_“ angefügt.

In Lambdaausdrücke eingeschlossene Variablen des umgebenden Gültigkeitsbereichs

Wenn ein Lambdaausdruck auf eine Variable verweist, die sich außerhalb des eigenen Definitionsbereichs befindet, schließt er eine Variable des umgebenden Gültigkeitsbereichs ein. Der Compiler muss diese Variable besonders berücksichtigen, da der Lambdaausdruck verwendet werden könnte, nachdem der Wert der Variablen geändert wurde. Der Lambdaausdruck muss also auf den neuen Wert verweisen. Umgekehrt könnte der Lambdaausdruck auch den Wert ändern. Diese Änderung sollte dann außerhalb des Lambdaausdrucks sichtbar sein. Im folgenden C#-Code

var i = 5;
Action lmbd = () => Console.WriteLine(i);
i = 6;
lmbd();

oder im folgenden Visual Basic-Code

Dim i = 5
Dim lmbd = Sub() Console.WriteLine(i)
i = 6
lmbd()

ist die erwartete Ausgabe 6 und nicht 5, da der Lambdaausdruck den Wert von i an der Stelle verwenden sollte, an dem der Lambdaausdruck aufgerufen wird, nachdem i auf 6 festgelegt wurde.

Der C#-Compiler erstellt hierzu eine anonyme Klasse mit den erforderlichen Variablen als Felder der Klasse und die Lambdaausdrücke als Methoden der Klasse. Anschließend ersetzt er alle Verweise auf diese Variable durch den Memberzugriff auf die Klasseninstanz. Die Klasseninstanz kann nicht geändert werden, der Wert der Felder hingegen schon. Die resultierende IL sieht in C# wie folgt aus:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0 {
  public int i;
  internal void <Main>b__0() => Console.WriteLine(i);
}
var @object = new <>c__DisplayClass0_0();
@object.i = 5;
Action lmbd = @object.<Main>b__0;
@object.i = 6;
lmbd();

Der Visual Basic-Compiler geht ähnlich vor, stellt jedoch dem Eigenschaftennamen „$VB$Local“ voran:

 

<CompilerGenerated> Friend NotInheritable Class _Closure$__0-0
  Public $VB$Local_i As Integer
  Sub _Lambda$__0()
    Console.WriteLine($VB$Local_i)
  End Sub
End Class
Dim targetObject = New _Closure$__0-0 targetObject With { .$VB$Local_i = 5 }
Dim lmbd = AddressOf targetObject. _Lambda$__0
targetObject.i = 6
lmbd()

„NameOf“-Operator Das Ergebnis des „NameOf“-Operators wird als „Constant“-Zeichenfolgenwert dargestellt.

Zeichenfolgeninterpolation und Boxing Hier findet eine Auflösung in einen Aufruf von „String.Format“ und in eine „Constant“-Formatzeichenfolge statt. Da der Interpolationsausdruck als Werttyp „Date“ (der Visual Basic-Alias für „DateTime“) typisiert ist, der entsprechende Parameter von „String.Format“ jedoch eine „Object“-Instanz erwartet, umschließt der Compiler auch den Interpolationsausdruck mit einem „Convert“-Knoten, aus dem sich „Object“ ergibt.

Aufrufe von Erweiterungsmethoden Hierbei handelt es sich um Aufrufe von Methoden auf Modulebene (statische Methoden in C#). Diese Aufrufe werden in Ausdrucksbaumstrukturen dargestellt. Auf Modulebene verwendete Methoden und „Shared“-Methoden verfügen über keine Instanz, sodass die „Object“-Eigenschaft von „MethodCallExpression“ „Nothing“ (oder NULL) zurückgibt. Wenn „MethodCallExpression“ einen Aufruf einer Instanzmethode darstellt, entspricht die „Object“-Eigenschaft nicht dem Wert „Nothing“.

Aus der Darstellung von Erweiterungsmethoden in Ausdrucksbaumstrukturen ergibt sich ein wichtiger Unterschied zwischen Ausdrucksbaumstrukturen und den Syntaxstrukturen der Roslyn-Compiler, die die Syntax unverändert beibehalten. In Ausdrucksbaumstrukturen wird der Fokus weniger auf eine genaue Darstellung der Syntax, sondern vielmehr auf die zugrunde liegenden Operationen gelegt. Die Roslyn-Syntaxstruktur für den Aufruf einer Erweiterungsmethode gleicht dem Aufruf einer Standardinstanzmethode, nicht aber dem Aufruf einer „Shared“- oder „static“-Methode.

Konvertierungen Wenn ein Ausdruckstyp nicht dem erwarteten Typ entspricht und der Ausdruckstyp implizit konvertiert wird, umschließt der Compiler den inneren Ausdruck mit einem „Convert“-Knoten, aus dem sich der erwartete Typ ergibt. Der Visual Basic-Compiler geht genauso vor, wenn er Ausdrucksbaumstrukturen mit einem Ausdruck erzeugt, dessen Typ den erwarteten Typ implementiert oder von diesem erbt. Auf Abbildung 4 ist beispielsweise zu sehen, dass die „Count“-Erweiterungsmethode eine Instanz von „IEnumerable(Of Char)“ erwartet. Der tatsächliche Typ des Ausdrucks ist jedoch „String“.

Erstellen von Ausdrucksbaumstruktur-Objekten (II): Verwenden der Factorymethoden

Sie können auch Ausdrucksbaumstrukturen mit den „Shared“- bzw. in C# „static“-Factorymethoden in „System.Linq.Expressions.Expression“ erstellen. Wenn Sie beispielsweise Ausdrucksbaumstruktur-Objekte für „i.ToString“ erstellen möchten, wobei i in Visual Basic dem Integer-Datentyp (in C# „int“) entspricht, können Sie den folgenden Code verwenden:

' Imports System.Linq.Expressions.Expression
Dim prm As ParameterExpression = Parameter(GetType(Integer), "i")
Dim expr As Expression = [Call](
  prm,
  GetType(Integer).GetMethods("ToString", {})
)

In C# sieht der Code wie folgt aus:

// Using static System.Linq.Expressions.Expression
ParameterExpression prm = Parameter(typeof(int), "i");
Expression expr = Call(
  prm,
  typeof(int).GetMethods("ToString", new [] {})
);

Wenn Sie auf diese Weise Ausdrucksbaumstrukturen erstellen, müssen Sie häufig Reflektion verwenden und mehrere Factorymethoden aufrufen. Dafür sind Sie jedoch flexibler und können die Ausdrucksbaumstruktur präzise Ihren Anforderungen anpassen. Wenn Sie die Compilersyntax verwenden würden, müssten Sie alle möglichen Variationen der Ausdrucksbaumstruktur für Ihr Programm ausschreiben.

Mithilfe der Factorymethoden-API können Sie außerdem Ausdrücke erstellen, die sich nicht in von Compilern generierten Ausdrücken verwenden lassen. Im Folgenden sehen Sie einige Beispiele:

Beispiel 1: Anweisungen, in denen beispielsweise ein „Conditional­Expression“-Objekt „System.Void“ zurückgibt. Dies entspricht „If … Then“ und „If … Then … Else … End“ (in C# „if (…) { … } else { … }“). Beispiel 2: ein „TryCatchExpression“-Objekt, das den Block „Try … Catch“ oder „try { … } catch (...) { ... }“ darstellt.

Assignments Dim x As Integer: x = 17.

Mehrere Anweisungen können mit Blöcken gruppiert werden.

Sehen Sie sich beispielsweise den folgenden Visual Basic-Code

Dim msg As String = "Hello!"
If DateTime.Now.Hour > 18 Then msg = "Good night"
Console.WriteLine(msg)

oder den folgenden gleichwertigen C#-Code an:

string msg = "Hello";
if (DateTime.Now.Hour > 18) {
  msg = "Good night";
}
Console.WriteLine(msg);

Sie können z. B. eine entsprechende Ausdrucksbaumstruktur in Visual Basic oder C# mithilfe der folgenden Factorymethoden erstellen (siehe Abbildung 5).

Abbildung 5: Blöcke, Zuweisungen und Anweisungen in Ausdrucksbaumstrukturen

' Imports System.Linq.Expressions.Expression
Dim msg = Parameter(GetType(String), "msg")
Dim body = Block(
  Assign(msg, Constant("Hello")),
  IfThen(
    GreaterThan(
      MakeMemberAccess(
        MakeMemberAccess(
          Nothing,
          GetType(DateTime).GetMember("Now").Single
        ),
        GetType(DateTime).GetMember("Hour").Single
      ),
      Constant(18)
    ),
    Assign(msg, Constant("Good night"))
  ),
  [Call](
    GetType(Console).GetMethod("WriteLine", { GetType(string) }),
    msg
  )
)
// Using static System.Linq.Expressions.Expression
var msg = Parameter(typeof(string), "msg");
var expr = Lambda(
  Block(
    Assign(msg, Constant("Hello")),
    IfThen(
      GreaterThan(
        MakeMemberAccess(
          MakeMemberAccess(
            null,
            typeof(DateTime).GetMember("Now").Single()
          ),
          typeof(DateTime).GetMember("Hour").Single()
        ),
        Constant(18)
      ),
      Assign(msg, Constant("Good night"))
    ),
    Call(
      typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }),
      msg
    )
  )
);

Verwenden von Ausdrucksbaumstrukturen (I): Zuordnen von Codekonstrukten zu externen APIs

Ausdrucksbaumstrukturen wurden ursprünglich entworfen, um die Visual Basic- oder C#-Syntax einer anderen API zuordnen zu können. In einem klassischen Anwendungsfall wird eine SQL-Anweisung wie folgt generiert:

SELECT * FROM Persons WHERE Persons.LastName LIKE N'D%'

Diese Anweisung kann aus Code wie dem im folgenden Codeausschnitt abgeleitet werden. Darin wird mithilfe des Punktoperators auf Member zugegriffen, Methoden werden innerhalb des Ausdrucks aufgerufen, und die „Queryable.Where“-Methode wird verwendet. In Visual Basic sieht der Code wie folgt aus:

Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.LastName.StartsWith("D"))

In C# wird folgender Code verwendet:

IQueryable<Person> personSource = ...
var qry = personSource.Where(x => x.LastName.StartsWith("D");

Wie funktioniert das? Für den Lambdaausdruck können die beiden Überladungen von „Enumerable.Where“ und „Queryable.Where“ verwendet werden. Bei der Überladungsauflösung wird jedoch die Überladung, in der der Lambdaausdruck ein Ausdruckslambda ist (also „Queryable.Where“), gegenüber der Überladung bevorzugt, die einen Delegaten akzeptiert. Der Compiler ersetzt dann die Lambdasyntax durch Aufrufe der entsprechenden Factorymethoden.

Zur Laufzeit umschließt die „Queryable.Where“-Methode die übergebene Ausdrucksbaumstruktur mit einem „Call“-Knoten, dessen „Method“-Eigenschaft auf „Queryable.Where“ selbst verweist. Zusätzlich akzeptiert der Knoten die beiden Parameter „personSource“ und die Ausdrucksbaumstruktur der Lambdasyntax (Abbildung 6). Der „Quote“-Knoten weist darauf hin, dass die innere Ausdrucksbaumstruktur „Queryable.Where“ als Ausdrucksbaumstruktur und nicht als Delegat übergeben wird.

Visualisierung der finalen Ausdrucksbaumstruktur
Abbildung 6: Visualisierung der finalen Ausdrucksbaumstruktur

Ein LINQ-Datenbankanbieter (z. B. Entity Framework, LINQ2­SQL oder NHibernate) kann eine solche Ausdrucksbaumstruktur annehmen und die verschiedenen Teile der SQL-Anweisung zuordnen, die am Anfang dieses Abschnitts gezeigt wurde. Das geht so:

  • „ExpresssionType.Call“ für „Queryable.Where“ wird als SQL-WHERE-Klausel analysiert.
  • „ExpressionType.MemberAccess“ für „LastName“ einer Instanz von „Person“ liest das „LastName“-Feld in der „Persons“-Tabelle über „Persons.LastName“.
  • „ExpressionType.Call“ für die „StartsWith“-Methode mit einem „Constant“-Argument wird in den SQL-LIKE-Operator übersetzt. Dabei wird ein Muster berücksichtigt, das mit dem Anfang einer konstanten Zeichenfolge übereinstimmt:
LIKE N'D%'

Es ist daher möglich, externe APIs mithilfe von Codekonstrukten und Konventionen zu steuern und dabei alle Vorteile des Compilers zu nutzen. Zu diesen gehören Typsicherheit, eine fehlerfreie Syntax für die verschiedenen Teile der Ausdrucksbaumstruktur und die automatische Vervollständigung in der IDE. Im Folgenden sehen Sie einige weitere Beispiele:

Erstellen von Webanforderungen Mithilfe der „Simple.OData.Client“-Bibliothek (bit.ly/2YyDrsx) können Sie „OData“-Anforderungen erstellen, indem Sie den verschiedenen Methoden Ausdrucksbaumstrukturen übergeben. Die Bibliothek gibt dann die richtige Anforderung aus. Abbildung 7 zeigt den Code in Visual Basic und C#.

Abbildung 7: Anforderungen mit der „Simple.OData.Client“-Bibliothek und Ausdrucksbaumstrukturen

Dim client = New ODataClient("https://services.odata.org/v4/TripPinServiceRW/")
Dim people = Await client.For(Of People)
  .Filter(Function(x) x.Trips.Any(Function(y) y.Budget > 3000))
  .Top(2)
  .Select(Function(x) New With { x.FirstName, x.LastName})
  .FindEntriesAsync
var client = new ODataClient("https://services.odata.org/v4/TripPinServiceRW/");
var people = await client.For<People>()
  .Filter(x => x.Trips.Any(y => y.Budget > 3000))
  .Top(2)
  .Select(x => new {x.FirstName, x.LastName})
  .FindEntriesAsync();

Hier ein Beispiel für eine ausgehende Anforderung:

 

> https://services.odata.org/v4/TripPinServiceRW/People?$top=2 &amp;
  $select=FirstName, LastName &amp; $filter=Trips/any(d:d/Budget gt 3000)

Reflektion anhand eines Beispiels Statt mit Reflektion ein „MethodInfo“-Objekt abzurufen, können Sie einen Methodenaufruf innerhalb eines Ausdrucks verwenden und diesen Ausdruck einer Funktion übergeben, die die im Aufruf verwendete Überladung extrahiert. Der Reflektionscode in Visual Basic sieht wie folgt aus:

Dim writeLine as MethodInfo = GetType(Console).GetMethod(
  "WriteLine", { GetType(String) })

Der gleichwertige Code in C# sieht so aus:

MethodInfo writeLine = typeof(Console).GetMethod(
  "WriteLine", new [] { typeof(string) });

In Visual Basic könnte die Funktion und deren Verwendung wie folgt aussehen:

Function GetMethod(expr As Expression(Of Action)) As MethodInfo
  Return CType(expr.Body, MethodCallExpression).Method
End Function
Dim mi As MethodInfo = GetMethod(Sub() Console.WriteLine(""))

Für C# ergibt sich z. B. Folgendes:

public static MethodInfo GetMethod(Expression<Action> expr) =>
  (expr.Body as MethodCallExpression).Method;
MethodInfo mi = GetMethod(() => Console.WriteLine(""));

Diese Vorgehensweise vereinfacht auch die Nutzung von Erweiterungsmethoden und die Erstellung von geschlossenen generischen Methoden, wie im folgenden Visual Basic-Code gezeigt wird:

Dim wherePerson As MethodInfo = GetMethod(Sub() CType(Nothing, IQueryable(Of
  Person)).Where(Function(x) True)))

Außerdem wird garantiert, dass die Methode und die Überladung zur Kompilierzeit existieren. Dies wird im folgenden C#-Code veranschaulicht:

 

// Won’t compile, because GetMethod expects Expression<Action>, not Expression<Func<..>>
MethodInfo getMethod = GetMethod(() => GetMethod(() => null));

Spaltenkonfiguration in einem Rasterlayout Wenn Sie eine Benutzeroberfläche verwenden, die auf einem Rasterlayout basiert, und Sie die Spalten deklarativ definieren wollen, können Sie eine Methode nutzen, die die Spalten auf Grundlage der Teilausdrücke in einem Arrayliteral erstellt. In Visual Basic kann dies wie folgt aussehen:

grid.SetColumns(Function(x As Person) {x.LastName, x.FirstName, x.DateOfBirth})

In C# können alternativ die Teilausdrücke eines anonymen Typs verwendet werden:

grid.SetColumns((Person x) => new {x.LastName, x.FirstName, DOB = x.DateOfBirth});

Verwenden von Ausdrucksbaumstrukturen (II): Kompilieren von aufrufbarem Code zur Laufzeit

Der zweite Hauptanwendungsfall für Ausdrucksbaumstrukturen besteht darin, ausführbaren Code zur Laufzeit zu generieren. So kann z. B. die oben verwendete „body“-Variable von einem „LambdaExpression“-Objekt umschlossen und zu einem Delegaten kompiliert werden, der anschließend aufgerufen wird. Alle Schritte werden zur Laufzeit durchgeführt. In Visual Basic sieht der Code wie folgt aus:

Dim lambdaExpression = Lambda(Of Action)(body)
Dim compiled = lambdaExpression.Compile
compiled.Invoke
' prints either "Hello" or "Good night"

Für C# ergibt sich Folgendes:

var lambdaExpression = Lambda<Action>(body);
var compiled = lambdaExpression.Compile();
compiled.Invoke();
// Prints either "Hello" or "Good night"

Das Kompilieren einer Ausdrucksbaumstruktur zu ausführbarem Code ist für die Implementierung anderer Sprachen auf Grundlage der CLR sehr nützlich, da es viel einfacher ist, mit Ausdrucksbaumstrukturen zu arbeiten, statt die IL direkt anzupassen. Wenn Sie in C# oder Visual Basic programmieren und die Programmlogik zur Kompilierzeit kennen, stellt sich die Frage, warum Sie diese Logik nicht zur Kompilierzeit in die vorhandene Methode oder Assembly einbetten sollten, statt sie zur Laufzeit zu kompilieren.

Die Kompilierung zur Laufzeit ist vor allem dann sinnvoll, wenn zur Entwurfszeit die ideale Vorgehensweise oder der richtige Algorithmus noch nicht bekannt ist. Mithilfe von Ausdrucksbaumstrukturen und der Kompilierung zur Laufzeit ist es relativ einfach, die Programmlogik als Reaktion auf echte Bedingungen oder Daten zur Laufzeit iterativ neu schreiben zu lassen und zu verbessern.

Selbstmodifizierender Code: Zwischenspeicherung an Aufrufstellen in dynamisch typisierten Sprachen Ein Beispiel für die Zwischenspeicherung an Aufrufstellen ist die Dynamic Language Runtime (DLR), die eine dynamische Typisierung in Sprachimplementierungen für die CLR ermöglicht. Die DLR verwendet Ausdrucksbaumstrukturen als leistungsstarkes Optimierungsverfahren, in dem ein Delegat, der einer bestimmten Aufrufstelle zugewiesen ist, bei Bedarf neu geschrieben wird.

Sowohl C# als auch Visual Basic sind größtenteils statisch typisierte Sprachen. Jeder Ausdruck kann zu einem Typ aufgelöst werden, der sich über die Lebensdauer des Programms nicht verändert. Anders ausgedrückt: Wenn die Variablen x und y als Integer (in C# „int“) deklariert wurden und das Programm die Codezeile „x + y“ enthält, wird für die Auflösung des Ausdruckswerts immer die „add“-Anweisung für zwei Integerwerte verwendet.

Dynamisch typisierte Sprachen stellen jedoch keine derartige Garantie bereit. Im Allgemeinen haben x und y keinen inhärenten Typ. Daher muss bei der Auswertung von „x + y“ berücksichtigt werden, dass x und y einem beliebigen Typ wie „String“ entsprechen können. Bei der Auflösung von „x + y“ wird dann in diesem Fall „String.Concat“ verwendet. Wenn das Programm hingegen zum ersten Mal auf den Ausdruck stößt und x und y Integerwerte sind, haben x und y bei darauffolgenden Vorkommnissen höchstwahrscheinlich denselben Typ. Die DLR nutzt dies mithilfe einer Speicherung an der Aufrufstelle aus. Dabei wird mit Ausdrucksbaumstrukturen der Delegat der Aufrufstelle immer dann neu geschrieben, wenn ein neuer Typ erkannt wird.

Jeder Aufrufstelle wird eine „CallSite(Of T)“-Instanz (in C# eine „CallSite<T>“-Instanz) mit einer „Target“-Eigenschaft zugewiesen, die auf einen kompilierten Delegaten verweist. Zusätzlich werden jeder Aufrufstelle mehrere Tests und Aktionen zugewiesen, wobei Letztere beim Bestehen der Tests ausgeführt werden. Für den „Target“-Delegaten liegt anfänglich nur Code vor, der für seine eigene Aktualisierung verwendet wird. Dieser kann z. B. wie folgt aussehen:

‘ Visual Basic code representation of Target delegate
Return site.Update(site, x, y)

Bei der ersten Iteration ruft die „Update“-Methode einen anwendbaren Test und eine entsprechende Aktion aus der Sprachimplementierung ab. Dabei wird beispielsweise festgelegt, dass die „add“-Anweisung verwendet wird, wenn beide Argumente Integerwerte sind. Anschließend wird eine Ausdrucksbaumstruktur generiert, die die Aktion nur dann ausführt, wenn der Test bestanden wird. Gleichwertiger Code für die resultierende Ausdrucksbaumstruktur könnte wie folgt aussehen:

‘ Visual Basic code representation of expression tree
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
Return site.Update(site, x, y)

Die Ausdrucksbaumstruktur wird dann zu einem neuen Delegaten kompiliert und in der „Target“-Eigenschaft gespeichert, während der Test und die Aktion im Objekt der Aufrufstelle gespeichert werden.

In späteren Iterationen wird an der Aufrufstelle der neue Delegat verwendet, um „x + y“ aufzulösen. Wenn der Test innerhalb des neuen Delegaten bestanden wird, wird die aufgelöste CLR-Operation verwendet. Nur wenn der Test fehlschlägt (also in diesem Fall weder x noch y ein Integerwert ist), muss die „Update“-Methode noch mal einen Aufruf für die Sprachimplementierung durchführen. In diesem Fall werden der neue Test und die Aktion hinzugefügt. Außerdem wird der „Target“-Delegat neu kompiliert, damit Test und Aktion berücksichtigt werden. Zu diesem Zeitpunkt enthält der „Target“-Delegat Tests für alle zuvor gefundenen Typpaare und die Wertauflösungsstrategie für jeden Typ, wie im folgenden Code gezeigt wird:

If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
If TypeOf x Is String Then Return String.Concat(x, y)
Return site.Update(site, x, y)

Ohne die Fähigkeit des Compilers, Code aus einer Ausdrucksbaumstruktur zu kompilieren, wäre ein solches Neuschreiben von Code zur Laufzeit als Reaktion auf sich verändernde Umstände nicht nur schwierig, sondern vermutlich sogar unmöglich.

Dynamische Kompilierung mit Ausdrucksbaumstrukturen und Roslyn im Vergleich Sie können dynamischen Code nicht nur aus Ausdrucksbaumstrukturen, sondern mit Roslyn auch aus einfachen Zeichenfolgen kompilieren. Diese Vorgehensweise ist zu bevorzugen, wenn Sie sich noch nicht genauer mit der Visual Basic- oder C#-Syntax befasst haben oder es wichtig ist, die erzeugte Syntax beizubehalten. Wie bereits erwähnt, modellieren Roslyn-Strukturen die Syntax, während Ausdrucksbaumstrukturen ausschließlich Codeoperationen ohne die Syntax darstellen.

Außerdem wird nur eine einzige Ausnahme ausgelöst, wenn Sie versuchen, eine ungültige Ausdrucksbaumstruktur zu erstellen. Wenn Zeichenfolgen mit Roslyn analysiert und zu ausführbarem Code kompiliert werden, sind Diagnoseinformationen zu unterschiedlichen Kompilierungsschritten genauso verfügbar wie beim Schreiben von C#- oder Visual Basic-Code in Visual Studio.

Roslyn ist jedoch eine umfassende Abhängigkeit, deren Einbindung in ein Projekt sich als kompliziert gestaltet. Möglicherweise verfügen Sie bereits über Codeoperationen, die aus einer anderen Quelle als dem C#- oder Visual Basic-Quellcode stammen. Ein Neuschreiben von Code unter Berücksichtigung des Roslyn-Semantikmodells ist dann unter Umständen nicht notwendig. Beachten Sie auch, dass Roslyn Multithreading erfordert und nicht verwendet werden kann, wenn neue Threads unzulässig sind (z. B. in einer Schnellansicht des Visual Studio-Debuggers).

Neuschreiben von Ausdrucksbaumstrukturen: Implementieren des Visual Basic-Operators „Like“

Wie bereits erwähnt, sind Ausdrucksbaumstrukturen unveränderlich. Sie können jedoch eine neue Ausdrucksbaumstruktur erstellen, die Teile der ursprünglichen Struktur wiederverwendet. Angenommen, Sie möchten aus einer Datenbank Einträge zu Personen abfragen, deren Vornamen ein „e“ und direkt danach ein „i“ enthalten. Der Visual Basic-Operator „Like“ gibt „True“ zurück, wenn eine Zeichenfolge mit einem Muster übereinstimmt. Dies sehen Sie im folgenden Beispiel:

Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.FirstName Like "*e*i*")
For Each person In qry
  Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next

Wenn Sie diesen Vorgang für ein „DbContext“-Objekt in Entity Framework 6 ausführen, wird eine Ausnahme mit der folgenden Meldung ausgelöst:

„LINQ to Entities does not recognize the method 'Boolean LikeString(System.String, System.String, Microsoft.VisualBasic.CompareMethod)' method, and this method cannot be translated into a store expression.“ (LINQ to Entities erkennt die Methode „Boolean LikeString(System.String, System.String, Microsoft.VisualBasic.CompareMethod)“ nicht. Diese Methode kann nicht in einen Speicherausdruck übersetzt werden.).

Der Visual Basic-Operator „Like“ wird in die Methode „LikeOperator.Like­String“ (im Namespace „Microsoft.VisualBasic.CompilerServices“) aufgelöst, die Entity Framework 6 nicht in einen SQL-LIKE-Ausdruck übersetzen kann. Daher tritt ein Fehler auf.

Entity Framework 6 unterstützt allerdings ähnliche Funktionen mithilfe der „DbFunctions.Like“-Methode, die dem entsprechenden LIKE-Ausdruck zugeordnet werden kann. Dazu müssen Sie die ursprüngliche Ausdrucksbaumstruktur, die den Visual Basic-Operator „Like“ verwendet, durch eine Ausdrucksbaumstruktur ersetzen, die „DbFunctions.Like“ verwendet. Dabei dürfen jedoch die anderen Teile der Struktur nicht verändert werden. Üblicherweise werden dazu Klassen erstellt, die von der .NET-Klasse „ExpressionVisitor“ erben. Anschließend werden die relevanten „Visit*“-Basismethoden überschrieben. Da im vorliegenden Fall ein Methodenaufruf ersetzt werden soll, wird „VisitMethodCall“ wie auf Abbildung 8 dargestellt überschrieben.

Abbildung 8: „ExpressionTreeVisitor“ ersetzt den Visual Basic-Operator „Like“ durch „DbFunctions.Like“

Class LikeVisitor
  Inherits ExpressionVisitor
  Shared LikeString As MethodInfo =
    GetType(CompilerServices.LikeOperator).GetMethod("LikeString")
  Shared DbFunctionsLike As MethodInfo = GetType(DbFunctions).GetMethod(
    "Like", {GetType(String), GetType(String)})
  Protected Overrides Function VisitMethodCall(
    node As MethodCallExpression) As Expression
    ' Is this node using the LikeString method? If not, leave it alone.
    If node.Method <> LikeString Then Return MyBase.VisitMethodCall(node)
    Dim patternExpression = node.Arguments(1)
    If patternExpression.NodeType = ExpressionType.Constant Then
      Dim oldPattern =
        CType(CType(patternExpression, ConstantExpression).Value, String)
      ' partial mapping of Visual Basic's Like syntax to SQL LIKE syntax
      Dim newPattern = oldPattern.Replace("*", "%")
      patternExpression = Constant(newPattern)
    End If
    Return [Call](DbFunctionsLike,
      node.Arguments(0),
      patternExpression
    )
  End Function
End Class

Die Mustersyntax des „Like“-Operators unterscheidet sich von der, die SQL LIKE verwendet. Deswegen werden die Sonderzeichen für den Visual Basic-Operator „Like“ durch diejenigen für SQL LIKE ersetzt. Diese Zuordnung ist unvollständig, da nicht die gesamte Mustersyntax des Visual Basic-Operators „Like“ zugeordnet wird. Außerdem werden SQL-LIKE-Sonderzeichen nicht mit einem Escapezeichen versehen bzw. Escapezeichen für Sonderzeichen des Visual Basic-Operators „Like“ entfernt. Eine vollständige Implementierung einschließlich der C#-Version finden Sie auf GitHub unter bit.ly/2yku7tx.

Beachten Sie, dass diese Zeichen nur ersetzt werden können, wenn das Muster Teil der Ausdrucksbaumstruktur und der Ausdrucksknoten „Constant“ ist. Wenn das Muster ein weiterer Ausdruckstyp ist – beispielsweise das Ergebnis eines Methodenaufrufs oder eines „BinaryExpression“-Objekts, das zwei weitere Zeichenfolgen verkettet –, wird der Wert des Musters erst erzeugt, nachdem der Ausdruck ausgewertet wurde.

Nun kann der Ausdruck durch den neu geschriebenen Ausdruck ersetzt werden, und der neue Ausdruck lässt sich wie folgt in der Abfrage verwenden:

Dim expr As Expression(Of Func(Of Person, Boolean)) =
  Function(x) x.FirstName Like "*e*i*"
Dim visitor As New LikeVisitor
expr = CType(visitor.Visit(expr), Expression(Of Func(Of Person, Boolean)))
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(expr)
For Each person In qry
  Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next

Im Idealfall sollte diese Art der Transformation innerhalb des LINQ to Entities-Anbieters durchgeführt werden. Dabei kann die gesamte Ausdrucksbaumstruktur, die weitere Ausdrucksbaumstrukturen und Aufrufe von „Queryable“-Methoden enthalten kann, auf einmal neu geschrieben werden. Es ist dann nicht notwendig, alle Ausdrücke neu zu schreiben, bevor diese den „Queryable“-Methoden übergeben werden. Die grundlegende Transformation ist jedoch identisch, da eine Klasse oder Funktion alle Knoten abruft und diese bei Bedarf durch andere Knoten ersetzt.

Zusammenfassung

Mit Ausdrucksbaumstrukturen lassen sich verschiedene Codeoperationen modellieren. Außerdem können APIs verfügbar gemacht werden, ohne dass Entwickler eine neue Programmiersprache oder ein neues Vokabular erlernen müssen. Die APIs können in Visual Basic oder C# verwendet werden, wobei der Compiler die Typen und die Syntax überprüft und die IDE IntelliSense zur Verfügung stellt. Eine geänderte Kopie einer Ausdrucksbaumstruktur kann erstellt werden, indem Knoten hinzugefügt, entfernt oder ersetzt werden. Ausdrucksbaumstrukturen können auch verwendet werden, um Code dynamisch zur Laufzeit zu kompilieren. Außerdem können sie sogar für selbstmodifizierenden Code eingesetzt werden. Für die dynamische Kompilierung empfiehlt sich aber dennoch Roslyn.

Die Codebeispiele für diesen Artikel finden Sie unter bit.ly/2yku7tx.

Weitere Informationen

  • Ausdrucksbaumstrukturen in den Programmierleitfäden für Visual Basic (bit.ly/2Msocef) und C# (bit.ly/2Y9q5nj).
  • Lambdaausdrücke in den Programmierleitfäden für Visual Basic (bit.ly/2YsZFs3) und C# (bit.ly/331ZWp5).
  • Bart De Smet: Projekt „Expression Tree API Futures“ (API-Verbesserungen für Ausdrucksbaumstrukturen) (bit.ly/2OrUsRw).
  • DLR-Projekt auf GitHub (Bit.ly/2yssz0x) mit Dokumenten, in denen der Entwurf von Ausdrucksbaumstrukturen in .NET beschrieben wird.
  • Darstellung von Ausdrucksbaumstrukturen als Zeichenfolgen und Schnellansichten in Debuggern für Ausdrucksbaumstrukturen (bit.ly/2MsoXnB und bit.ly/2GAp5ha).
  • StackOverflow: „What does Expression.Quote() do that Expression.Constant() can’t already do?“ (Inwiefern unterscheidet sich „Expression.Quote()“ von „Expression.Constant()“?) (bit.ly/30YT6Pi).

ZEV Spitzist Entwickler einer Bibliothek, mit der sich Ausdrucksbaumstrukturen als Zeichenfolgen in C#, Visual Basic oder in Aufrufen von Factorymethoden darstellen lassen. Außerdem hat er für Ausdrucksbaumstrukturen eine Schnellansicht für das Debuggen in Visual Studio entworfen.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Kathleen Dollard


Diesen Artikel im MSDN Magazine-Forum diskutieren