Warum sollten Sie Ausdruck <Func <T>> anstelle von Func <T> verwenden?

949

Ich verstehe Lambdas und die Funcund ActionDelegierten. Aber Ausdrücke überraschen mich.

Unter welchen Umständen würden Sie Expression<Func<T>>eher eine als eine einfache alte verwenden Func<T>?

Richard Nagle
quelle
14
Func <> wird auf der c # -Compilerebene in eine Methode konvertiert, Expression <Func <>> wird auf MSIL-Ebene ausgeführt, nachdem der Code direkt kompiliert wurde. Dies ist der Grund, warum er schneller ist
Waleed AK
1
Zusätzlich zu den Antworten ist die csharp-Sprachspezifikation "4.6 Ausdrucksbaumtypen" hilfreich, um Querverweise zu
erstellen

Antworten:

1133

Wenn Sie Lambda-Ausdrücke als Ausdrucksbäume behandeln und in sie schauen möchten, anstatt sie auszuführen. Beispielsweise ruft LINQ to SQL den Ausdruck ab und konvertiert ihn in die entsprechende SQL-Anweisung und sendet ihn an den Server (anstatt das Lambda auszuführen).

Konzeptionell Expression<Func<T>>ist völlig anders als Func<T>. Func<T>bezeichnet a, delegatedas so ziemlich ein Zeiger auf eine Methode ist, und Expression<Func<T>>bezeichnet eine Baumdatenstruktur für einen Lambda-Ausdruck. Diese Baumstruktur beschreibt, was ein Lambda-Ausdruck tut, anstatt das eigentliche zu tun. Es enthält im Wesentlichen Daten über die Zusammensetzung von Ausdrücken, Variablen, Methodenaufrufen usw. (zum Beispiel enthält es Informationen wie dieses Lambda ist eine Konstante + ein Parameter). Sie können diese Beschreibung verwenden, um sie in eine tatsächliche Methode (mit Expression.Compile) zu konvertieren oder andere Dinge (wie das Beispiel LINQ to SQL) damit zu tun. Die Behandlung von Lambdas als anonyme Methoden und Ausdrucksbäume ist eine reine Sache zur Kompilierungszeit.

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

wird effektiv zu einer IL-Methode kompiliert, die nichts erhält und 10 zurückgibt.

Expression<Func<int>> myExpression = () => 10;

wird in eine Datenstruktur konvertiert, die einen Ausdruck beschreibt, der keine Parameter erhält und den Wert 10 zurückgibt:

Ausdruck gegen Func größeres Bild

Während beide zur Kompilierungszeit gleich aussehen, ist das, was der Compiler generiert, völlig anders .

Mehrdad Afshari
quelle
96
Mit anderen Worten, an Expressionenthält die Metainformationen zu einem bestimmten Delegaten.
Bertl
40
@bertl Eigentlich nein. Der Delegierte ist überhaupt nicht beteiligt. Der Grund, warum es überhaupt eine Zuordnung zu einem Delegaten gibt, ist, dass Sie den Ausdruck zu einem Delegaten kompilieren können - oder genauer gesagt, zu einer Methode kompilieren und den Delegaten zu dieser Methode als Rückgabewert erhalten können. Der Ausdrucksbaum selbst besteht jedoch nur aus Daten. Der Delegat existiert nicht, wenn Sie Expression<Func<...>>statt nur verwenden Func<...>.
Luaan
5
@Kyle Delaney Ein (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }solcher Ausdruck ist ein ExpressionTree. Für die If-Anweisung werden Verzweigungen erstellt.
Matteo Marciano - MSCP
3
@bertl Delegate ist das, was die CPU sieht (ausführbarer Code einer Architektur), Expression ist das, was der Compiler sieht (lediglich ein anderes Format des Quellcodes, aber immer noch Quellcode).
Codewarrior
5
@bertl: Es könnte genauer zusammengefasst werden, wenn man sagt, dass ein Ausdruck für eine Funktion ist, was ein Stringbuilder für einen String ist. Es ist keine Zeichenfolge / Funktion, aber es enthält die erforderlichen Daten, um eine zu erstellen, wenn Sie dazu aufgefordert werden.
Flater
337

Ich füge eine Antwort für Noobs hinzu, weil diese Antworten über meinem Kopf schienen, bis mir klar wurde, wie einfach es ist. Manchmal ist es Ihre Erwartung, dass es kompliziert ist, dass Sie nicht in der Lage sind, Ihren Kopf darum zu wickeln.

Ich musste den Unterschied nicht verstehen, bis ich auf einen wirklich nervigen "Fehler" stieß, der versuchte, LINQ-to-SQL generisch zu verwenden:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

Dies funktionierte hervorragend, bis ich anfing, OutofMemoryExceptions für größere Datensätze zu erhalten. Durch das Setzen von Haltepunkten innerhalb des Lambda wurde mir klar, dass es nacheinander durch jede Zeile in meiner Tabelle iterierte, um nach Übereinstimmungen mit meinem Lambda-Zustand zu suchen. Das hat mich eine Weile verblüfft, denn warum zum Teufel behandelt es meine Datentabelle als eine riesige IEnumerable, anstatt LINQ-to-SQL so zu machen, wie es soll? Genau das Gleiche tat es auch in meinem LINQ-to-MongoDb-Gegenstück.

Das Update war einfach zu drehen Func<T, bool>in Expression<Func<T, bool>>, so dass ich gegoogelt , warum es eine braucht Expressionstatt Func, endet hier oben.

Ein Ausdruck verwandelt einen Delegaten einfach in Daten über sich. So a => a + 1wird so etwas wie "Auf der linken Seite gibt es eine int a. Auf der rechten Seite addieren Sie 1 dazu." Das ist es. Du kannst jetzt nach Hause gehen. Es ist offensichtlich strukturierter als das, aber das ist im Wesentlichen alles, was ein Ausdrucksbaum wirklich ist - nichts, um den man den Kopf wickeln könnte.

Wenn man das versteht, wird klar, warum LINQ-to-SQL eine benötigt Expressionund eine Funcnicht ausreicht. Funcbringt keine Möglichkeit mit sich, in sich selbst hineinzukommen, um zu sehen, wie man es in eine SQL / MongoDb / andere Abfrage übersetzt. Sie können nicht sehen, ob es sich um Addition, Multiplikation oder Subtraktion handelt. Alles was Sie tun können, ist es auszuführen. ExpressionAuf der anderen Seite können Sie in den Delegaten schauen und alles sehen, was er tun möchte. Auf diese Weise können Sie den Delegaten in eine beliebige SQL-Abfrage übersetzen. Funchat nicht funktioniert, weil mein DbContext für den Inhalt des Lambda-Ausdrucks blind war. Aus diesem Grund konnte der Lambda-Ausdruck nicht in SQL umgewandelt werden. Es hat jedoch das nächstbeste getan und diese Bedingung durch jede Zeile in meiner Tabelle wiederholt.

Edit: auf Wunsch von John Peter meinen letzten Satz erläutern:

IQueryable erweitert IEnumerable, sodass IEnumerables Methoden wie Where()das Erhalten akzeptabler Überladungen Expression. Wenn Sie eine Expressionan diese übergeben, behalten Sie als Ergebnis eine IQueryable bei, aber wenn Sie eine übergeben Func, greifen Sie auf die Basis-IEnumerable zurück und erhalten als Ergebnis eine IEnumerable. Mit anderen Worten, ohne es zu bemerken, haben Sie Ihr Dataset in eine Liste umgewandelt, die iteriert werden soll, anstatt etwas abzufragen. Es ist schwer, einen Unterschied zu bemerken, bis man die Unterschriften wirklich unter die Haube schaut.

Chad Hedgcock
quelle
2
Tschad; Bitte erläutern Sie diesen Kommentar etwas genauer: "Func hat nicht funktioniert, weil mein DbContext blind für das war, was tatsächlich im Lambda-Ausdruck enthalten war, um ihn in SQL umzuwandeln. Er hat also das nächstbeste getan und diese Bedingung durch jede Zeile in meiner Tabelle wiederholt . "
John Peters
2
>> Func ... Alles was Sie tun können, ist es auszuführen. Es ist nicht genau richtig, aber ich denke, das ist der Punkt, der hervorgehoben werden sollte. Funktionen / Aktionen müssen ausgeführt werden, Ausdrücke müssen analysiert werden (vor dem Ausführen oder sogar anstatt ausgeführt zu werden).
Konstantin
@Chad Ist das Problem hier das?: Db.Set <T> hat die gesamte Datenbanktabelle abgefragt und danach, weil .Where (conditionLambda) die Where-Erweiterungsmethode Where (IEnumerable) verwendet hat, die für die gesamte Tabelle im Speicher aufgelistet wird . Ich denke, Sie erhalten OutOfMemoryException, weil dieser Code versucht hat, die gesamte Tabelle in den Speicher zu laden (und natürlich die Objekte erstellt hat). Habe ich recht? Danke :)
Bence Végert
104

Eine äußerst wichtige Überlegung bei der Auswahl von Expression vs Func ist, dass IQueryable-Anbieter wie LINQ to Entities das, was Sie in einem Expression übergeben, "verdauen" können, aber ignorieren, was Sie in einem Func übergeben. Ich habe zwei Blog-Beiträge zu diesem Thema:

Mehr zu Expression vs Func mit Entity Framework und Verlieben in LINQ - Teil 7: Ausdrücke und Funktionen (letzter Abschnitt)

LSpencer777
quelle
+ l zur Erklärung. Ich erhalte jedoch 'Der LINQ-Ausdrucksknotentyp' Aufrufen 'wird in LINQ to Entities nicht unterstützt.' und musste ForEach verwenden, nachdem die Ergebnisse abgerufen wurden.
Tymtam
77

Ich möchte einige Anmerkungen zu den Unterschieden zwischen Func<T>und hinzufügen Expression<Func<T>>:

  • Func<T> ist nur ein normales MulticastDelegate der alten Schule;
  • Expression<Func<T>> ist eine Darstellung des Lambda-Ausdrucks in Form eines Ausdrucksbaums;
  • Der Ausdrucksbaum kann über die Lambda-Ausdruckssyntax oder über die API-Syntax erstellt werden.
  • Der Ausdrucksbaum kann an einen Delegaten kompiliert werden Func<T>.
  • Die inverse Konvertierung ist theoretisch möglich, aber es ist eine Art Dekompilierung. Dafür gibt es keine eingebaute Funktionalität, da dies kein einfacher Prozess ist.
  • Ausdrucksbaum kann beobachtet / übersetzt / modifiziert werden durch ExpressionVisitor;
  • Die Erweiterungsmethoden für IEnumerable arbeiten mit Func<T>;
  • Die Erweiterungsmethoden für IQueryable arbeiten mit Expression<Func<T>>.

Es gibt einen Artikel, der die Details mit Codebeispielen beschreibt:
LINQ: Func <T> vs. Expression <Func <T>> .

Hoffe es wird hilfreich sein.

Olexander Ivanitskyi
quelle
Schöne Liste, eine kleine Anmerkung ist, dass Sie erwähnen, dass die inverse Konvertierung möglich ist, eine genaue Inverse jedoch nicht. Einige Metadaten gehen während des Konvertierungsprozesses verloren. Sie können es jedoch in einen Ausdrucksbaum dekompilieren, der beim erneuten Kompilieren dasselbe Ergebnis liefert.
Aidiakapi
76

Es gibt eine philosophischere Erklärung dafür aus Krzysztof Cwalinas Buch ( Framework Design Guidelines: Konventionen, Redewendungen und Muster für wiederverwendbare .NET-Bibliotheken );

Rico Mariani

Bearbeiten für Nicht-Bildversion:

In den meisten Fällen möchten Sie Func oder Action, wenn nur Code ausgeführt werden muss. Sie benötigen Expression, wenn der Code analysiert, serialisiert oder optimiert werden muss, bevor er ausgeführt wird. Ausdruck dient zum Nachdenken über Code, Func / Action zum Ausführen.

Oğuzhan Soykan
quelle
10
Gut ausgedrückt. dh. Sie benötigen einen Ausdruck, wenn Sie erwarten, dass Ihr Func in eine Art Abfrage konvertiert wird. Dh. Sie müssen database.data.Where(i => i.Id > 0)ausgeführt werden als SELECT FROM [data] WHERE [id] > 0. Wenn Sie nur in einem Func passieren, haben Sie Scheuklappen auf Ihrem Fahrer setzen und alles was man tun kann , ist , SELECT *und dann , wenn es all diese Daten in den Speicher geladen wird , durchlaufen jede und ausfiltern alles mit id> 0. Ihre Verpackung Funcin Expressionermächtigt der Treiber, um das zu analysieren Funcund es in eine SQL / MongoDb / andere Abfrage umzuwandeln.
Chad Hedgcock
Also, wenn ich für einen Urlaub plane, würde ich verwenden, Expressionaber wenn ich im Urlaub bin, wird es sein Func/Action;)
GoldBishop
1
@ChadHedgcock Dies war das letzte Stück, das ich brauchte. Vielen Dank. Ich habe mir das schon eine Weile angesehen, und Ihr Kommentar hier hat die ganze Studie zum Klicken gebracht.
Johnny
37

LINQ ist das kanonische Beispiel (zum Beispiel das Sprechen mit einer Datenbank), aber in Wahrheit geht es Ihnen immer mehr darum, auszudrücken, was zu tun ist, als es tatsächlich zu tun. Zum Beispiel verwende ich diesen Ansatz im RPC-Stapel von protobuf-net (um Codegenerierung usw. zu vermeiden) - also rufen Sie eine Methode auf mit:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

Dadurch wird der aufzulösende Ausdrucksbaum SomeMethod(und der Wert jedes Arguments) dekonstruiert , der RPC-Aufruf ausgeführt, any ref/ outargs aktualisiert und das Ergebnis des Remote-Aufrufs zurückgegeben. Dies ist nur über den Ausdrucksbaum möglich. Ich beschreibe das hier mehr .

Ein anderes Beispiel ist, wenn Sie die Ausdrucksbäume manuell erstellen, um sie zu einem Lambda zu kompilieren, wie dies durch den Code der generischen Operatoren erfolgt .

Marc Gravell
quelle
20

Sie würden einen Ausdruck verwenden, wenn Sie Ihre Funktion als Daten und nicht als Code behandeln möchten. Sie können dies tun, wenn Sie den Code (als Daten) bearbeiten möchten. Wenn Sie keinen Bedarf an Ausdrücken sehen, müssen Sie wahrscheinlich keinen verwenden.

Andrew Hare
quelle
19

Der Hauptgrund ist, wenn Sie den Code nicht direkt ausführen, sondern überprüfen möchten. Dies kann verschiedene Gründe haben:

  • Zuordnen des Codes zu einer anderen Umgebung (z. B. C # -Code zu SQL in Entity Framework)
  • Ersetzen von Teilen des Codes zur Laufzeit (dynamische Programmierung oder sogar einfache DRY-Techniken)
  • Code-Validierung (sehr nützlich beim Emulieren von Skripten oder bei der Analyse)
  • Serialisierung - Ausdrücke können relativ einfach und sicher serialisiert werden, Delegierte nicht
  • Stark typisierte Sicherheit für Dinge, die nicht von Natur aus stark typisiert sind, und Ausnutzung von Compilerprüfungen, obwohl Sie zur Laufzeit dynamische Aufrufe ausführen (ASP.NET MVC 5 mit Razor ist ein gutes Beispiel)
Luaan
quelle
Können
@ uowzd01 Schauen Sie sich Razor an - es verwendet diesen Ansatz ausgiebig.
Luaan
@Luaan Ich suche nach Ausdrucksserialisierungen, kann aber ohne eine eingeschränkte Verwendung durch Dritte nichts finden. Unterstützt .Net 4.5 die Serialisierung von Ausdrucksbäumen?
Vabii
@vabii Nicht das ich wüsste - und es wäre nicht wirklich eine gute Idee für den allgemeinen Fall. Mein Punkt war mehr, dass Sie in der Lage sind, eine ziemlich einfache Serialisierung für die spezifischen Fälle zu schreiben, die Sie unterstützen möchten, und zwar gegen vorab entworfene Schnittstellen - genau das habe ich einige Male getan. Im allgemeinen Fall Expressionkann es genauso unmöglich sein, ein zu delegieren wie ein Delegat, da jeder Ausdruck einen Aufruf einer beliebigen Delegaten- / Methodenreferenz enthalten kann. "Einfach" ist natürlich relativ.
Luaan
15

Ich sehe noch keine Antworten, die die Leistung erwähnen. Es ist schlecht, Func<>s zu übergeben Where()oder Count(). Wirklich schlecht. Wenn Sie a verwenden, wird stattdessen Func<>das IEnumerableLINQ-Zeug aufgerufen IQueryable, was bedeutet, dass ganze Tabellen eingezogen und dann gefiltert werden. Expression<Func<>>ist erheblich schneller, insbesondere wenn Sie eine Datenbank abfragen, auf der sich ein anderer Server befindet.

mhenry1384
quelle
Gilt dies auch für In-Memory-Abfragen?
stt106
@ stt106 Wahrscheinlich nicht.
Mhenry1384
Dies gilt nur, wenn Sie die Liste auflisten. Wenn Sie GetEnumerator oder foreach verwenden, wird die ienumerable nicht vollständig in den Speicher geladen.
Nelsontruran
1
@ stt106 Bei Übergabe an die .Where () -Klausel einer Liste <> wird Expression <Func <>> mit .Compile () aufgerufen, sodass Func <> mit ziemlicher Sicherheit schneller ist. Siehe referencesource.microsoft.com/#System.Core/System/Linq/…
NStuke