Perché dovresti usare Expression <Func <T>> anziché Func <T>?


949

Capisco lambda e l' Funce Actiondelegati. Ma le espressioni mi sconcertano.

In quali circostanze Expression<Func<T>>useresti un vecchio piuttosto che un semplice Func<T>?


14
Func <> verrà convertito in un metodo a livello di compilatore c #, Expression <Func <>> verrà eseguito a livello MSIL dopo aver compilato direttamente il codice, ecco perché è più veloce
Waleed AK,

1
oltre alle risposte, la specifica del linguaggio csharp "4.6 tipi di alberi delle espressioni" è utile per il riferimento incrociato
djeikyb

Risposte:


1133

Quando vuoi trattare le espressioni lambda come alberi delle espressioni e guardare al loro interno invece di eseguirle. Ad esempio, LINQ to SQL ottiene l'espressione e la converte in un'istruzione SQL equivalente e la inoltra al server (anziché eseguire lambda).

Concettualmente, Expression<Func<T>>è completamente diverso da Func<T>. Func<T>indica un delegateche è praticamente un puntatore a un metodo e Expression<Func<T>>indica una struttura di dati ad albero per un'espressione lambda. Questa struttura ad albero descrive cosa fa un'espressione lambda piuttosto che fare la cosa reale. In sostanza contiene dati sulla composizione di espressioni, variabili, chiamate di metodi, ... (ad esempio contiene informazioni come questa lambda è una costante + alcuni parametri). Puoi usare questa descrizione per convertirla in un metodo effettivo (con Expression.Compile) o fare altre cose (come l'esempio LINQ to SQL) con esso. L'atto di trattare i lambda come metodi anonimi e alberi di espressione è puramente una cosa del tempo di compilazione.

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

si compila efficacemente in un metodo IL che non ottiene nulla e restituisce 10.

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

verrà convertito in una struttura di dati che descrive un'espressione che non ottiene parametri e restituisce il valore 10:

Expression vs Func immagine più grande

Mentre entrambi sembrano uguali in fase di compilazione, ciò che genera il compilatore è totalmente diverso .


96
Quindi, in altre parole, un Expressioncontiene le meta-informazioni su un determinato delegato.
bertl,

40
@bertl In realtà, no. Il delegato non è coinvolto affatto. Il motivo per cui esiste un'associazione con un delegato è che è possibile compilare l'espressione in un delegato o, per essere più precisi, compilarlo in un metodo e ottenere il delegato in quel metodo come valore di ritorno. Ma l'albero delle espressioni stesso è solo un dato. Il delegato non esiste quando si utilizza Expression<Func<...>>anziché solo Func<...>.
Luaan,

5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }tale espressione è un ExpressionTree, i rami sono creati per l'istruzione If.
Matteo Marciano - MSCP

3
@bertl Delegate è ciò che vede la CPU (codice eseguibile di un'architettura), Expression è ciò che vede il compilatore (semplicemente un altro formato di codice sorgente, ma comunque codice sorgente).
codewarrior,

5
@bertl: potrebbe essere sintetizzato più accuratamente dicendo che un'espressione è per una funzione ciò che un costruttore di stringhe è per una stringa. Non è una stringa / funzione, ma contiene i dati necessari per crearne uno quando viene richiesto.
Flater

337

Sto aggiungendo una risposta per nessuno perché queste risposte mi sono sembrate sopra la mia testa, fino a quando ho capito quanto fosse semplice. A volte è la tua aspettativa che sia complicata che ti rende incapace di "avvolgerci la testa".

Non ho avuto bisogno di capire la differenza finché non sono entrato in un 'bug' davvero fastidioso, cercando di usare LINQ-to-SQL in modo generico:

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

Ha funzionato alla grande fino a quando non ho iniziato a ottenere OutofMemoryExceptions su set di dati più grandi. L'impostazione di punti di interruzione all'interno della lambda mi ha fatto capire che stava scorrendo una riga alla volta in ogni riga del mio tavolo alla ricerca di corrispondenze alla mia condizione lambda. Questo mi ha sconcertato per un po ', perché diamine sta trattando la mia tabella di dati come un gigantesco IEnumerable invece di fare LINQ-to-SQL come dovrebbe? Stava anche facendo la stessa identica cosa nella mia controparte LINQ-to-MongoDb.

La correzione doveva semplicemente trasformarsi Func<T, bool>in Expression<Func<T, bool>>, quindi ho cercato su Google perché ha bisogno di un Expressioninvece di Func, finendo qui.

Un'espressione trasforma semplicemente un delegato in un dato su se stesso. Quindi a => a + 1diventa qualcosa del tipo "Sul lato sinistro c'è un int a. Sul lato destro ne aggiungi 1". Questo è tutto. Puoi andare a casa ora. È ovviamente più strutturato di così, ma in sostanza è tutto ciò che un albero delle espressioni è davvero - niente che ti avvolga la testa.

Comprendendolo, diventa chiaro perché LINQ-to-SQL ha bisogno di un Expressione un Funcnon è adeguato. Funcnon porta con sé un modo per entrare in se stesso, per vedere il nocciolo di come tradurlo in una query SQL / MongoDb / altro. Non puoi vedere se sta eseguendo addizioni, moltiplicazioni o sottrazioni. Tutto quello che puoi fare è eseguirlo. Expressiond'altra parte, ti permette di guardare all'interno del delegato e vedere tutto ciò che vuole fare. Ciò ti consente di tradurre il delegato in qualsiasi cosa tu voglia, come una query SQL. Funcnon ha funzionato perché il mio DbContext era cieco al contenuto dell'espressione lambda. Per questo motivo, non è stato possibile trasformare l'espressione lambda in SQL; tuttavia, ha fatto la cosa migliore successiva e ha ripetuto tale condizione in ogni riga della mia tabella.

Modifica: esponendo la mia ultima frase su richiesta di John Peter:

IQueryable estende IEnumerable, quindi i metodi di IEnumerable come Where()ottenere sovraccarichi che accettano Expression. Quando si passa Expressiona quello, si mantiene un IQueryable come risultato, ma quando si passa a Func, si ricade sulla base IEnumerable e di conseguenza si ottiene un IEnumerable. In altre parole, senza notare che hai trasformato il tuo set di dati in un elenco per essere iterato anziché qualcosa da interrogare. È difficile notare una differenza fino a quando non guardi davvero sotto il cofano le firme.


2
Chad; Spiega un po 'di più questo commento: "Func non ha funzionato perché il mio DbContext era cieco a ciò che era effettivamente nell'espressione lambda per trasformarlo in SQL, quindi ha fatto la cosa migliore successiva e ha ripetuto il condizionale in ogni riga della mia tabella ".
John Peters,

2
>> Func ... Tutto quello che puoi fare è eseguirlo. Non è esattamente vero, ma penso che questo sia il punto da sottolineare. Funzioni / azioni devono essere eseguite, le espressioni devono essere analizzate (prima di essere eseguite o addirittura invece di essere eseguite).
Konstantin,

@Chad Il problema qui è che ?: db.Set <T> ha interrogato tutta la tabella del database, e dopo, perché .Where (conditionLambda) ha usato il metodo di estensione Where (IEnumerable), che è elencato sull'intera tabella nella memoria . Penso che ottieni OutOfMemoryException perché, questo codice ha cercato di caricare l'intera tabella nella memoria (e, naturalmente, ha creato gli oggetti). Ho ragione? Grazie :)
Bence Végert,

104

Una considerazione estremamente importante nella scelta di Expression vs Func è che i provider IQueryable come LINQ to Entities possono "digerire" ciò che si passa in un'espressione, ma ignorano ciò che si passa in un Func. Ho due post sul blog sull'argomento:

Altro su Expression vs Func con Entity Framework e Innamorarsi di LINQ - Parte 7: Expressions and Funcs (l'ultima sezione)


+ l per spiegazione. Tuttavia ottengo "Il tipo di nodo di espressione LINQ" Invoke "non è supportato in LINQ to Entities." e ho dovuto usare ForEach dopo aver recuperato i risultati.
tymtam,

77

Vorrei aggiungere alcune note sulle differenze tra Func<T>e Expression<Func<T>>:

  • Func<T> è solo un normale delegato Multicast di vecchia scuola;
  • Expression<Func<T>> è una rappresentazione dell'espressione lambda in forma di albero delle espressioni;
  • l'albero delle espressioni può essere costruito attraverso la sintassi delle espressioni lambda o tramite la sintassi dell'API;
  • l'albero delle espressioni può essere compilato per un delegato Func<T>;
  • la conversione inversa è teoricamente possibile, ma è una specie di decompilazione, non esiste una funzionalità integrata per questo in quanto non è un processo semplice;
  • l'albero delle espressioni può essere osservato / tradotto / modificato tramite ExpressionVisitor;
  • i metodi di estensione per IEnumerable funzionano con Func<T>;
  • i metodi di estensione per IQueryable funzionano con Expression<Func<T>>.

C'è un articolo che descrive i dettagli con esempi di codice:
LINQ: Func <T> vs. Expression <Func <T>> .

Spero possa essere utile.


Bella lista, una piccola nota è che dici che la conversione inversa è possibile, tuttavia non è esattamente un contrario. Alcuni metadati vengono persi durante il processo di conversione. Tuttavia, è possibile decompilarlo in un albero delle espressioni che produce lo stesso risultato una volta compilato di nuovo.
Aidiakapi,

76

C'è una spiegazione più filosofica al riguardo dal libro di Krzysztof Cwalina ( Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries );

Rico Mariani

Modifica per versione non immagine:

La maggior parte delle volte vorrete Func o Action se tutto ciò che deve accadere è eseguire un po 'di codice. È necessario Expression quando il codice deve essere analizzato, serializzato o ottimizzato prima di essere eseguito. L'espressione è per pensare al codice, Func / Action è per eseguirlo.


10
Ben messo. vale a dire. Hai bisogno di espressione quando aspetti che Func venga convertito in una sorta di query. Vale a dire. devi database.data.Where(i => i.Id > 0)essere eseguito come SELECT FROM [data] WHERE [id] > 0. Se passi semplicemente a Func, hai messo i paraocchi sul tuo driver e tutto ciò che può fare è SELECT *e poi, una volta caricati tutti quei dati in memoria, scorrere tutti e filtrare tutto con id> 0. Avvolgendo i tuoi Funcin Expressionpotenziatori il driver per analizzare Funce trasformarlo in una query Sql / MongoDb / other.
Chad Hedgcock,

Quindi, quando sto programmando una vacanza, ExpressionFunc/Action
userei

1
@ChadHedgcock Questo è stato l'ultimo pezzo di cui avevo bisogno. Grazie. Lo sto osservando da un po 'di tempo e il tuo commento qui ha fatto scattare tutto lo studio.
johnny

37

LINQ è l'esempio canonico (ad esempio, parlare con un database), ma in verità, ogni volta che ti interessa di più esprimere cosa fare, piuttosto che farlo effettivamente. Ad esempio, utilizzo questo approccio nello stack RPC di protobuf-net (per evitare la generazione di codice, ecc.), Quindi chiamate un metodo con:

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

Ciò decostruisce l'albero delle espressioni da risolvere SomeMethod(e il valore di ciascun argomento), esegue la chiamata RPC, aggiorna qualsiasi ref/ outargs e restituisce il risultato dalla chiamata remota. Questo è possibile solo tramite l'albero delle espressioni. Lo tratterò di più qui .

Un altro esempio è quando si creano manualmente gli alberi delle espressioni allo scopo di compilare un lambda, come fatto dal codice generico degli operatori .


20

Utilizzeresti un'espressione quando vuoi trattare la tua funzione come dati e non come codice. Puoi farlo se vuoi manipolare il codice (come dati). Il più delle volte se non vedi la necessità di espressioni, probabilmente non devi usarne una.


19

Il motivo principale è quando non si desidera eseguire direttamente il codice, ma piuttosto si desidera ispezionarlo. Questo può essere per qualsiasi numero di motivi:

  • Mappatura del codice su un altro ambiente (es. Codice C # su SQL in Entity Framework)
  • Sostituzione di parti del codice in runtime (programmazione dinamica o anche semplici tecniche DRY)
  • Convalida del codice (molto utile durante l'emulazione di script o durante l'analisi)
  • Serializzazione: le espressioni possono essere serializzate in modo piuttosto semplice e sicuro, i delegati no
  • Sicurezza fortemente tipizzata su cose che non sono intrinsecamente fortemente tipizzate e sfruttamento dei controlli del compilatore anche se si stanno effettuando chiamate dinamiche in runtime (ASP.NET MVC 5 con Razor è un buon esempio)

puoi approfondire un po 'di più il n.5
uowzd01

@ uowzd01 Basta guardare Razor: utilizza ampiamente questo approccio.
Luaan,

@Luaan Sto cercando serializzazioni di espressioni ma non riesco a trovare nulla senza un uso limitato di terze parti. .Net 4.5 supporta la serializzazione dell'albero delle espressioni?
vabii

@vabii Non che io sappia - e non sarebbe davvero una buona idea per il caso generale. Il mio punto era di più sul fatto che sei in grado di scrivere una serializzazione piuttosto semplice per i casi specifici che vuoi supportare, rispetto alle interfacce progettate in anticipo - l'ho fatto solo un paio di volte. Nel caso generale, un Expressionpuò essere altrettanto impossibile da serializzare come un delegato, poiché qualsiasi espressione può contenere un'invocazione di un delegato / metodo di riferimento arbitrario. "Facile" è relativo, ovviamente.
Luaan,

15

Non vedo ancora risposte che menzionino la performance. Passare Func<>s in Where()o Count()è male. Davvero male. Se usi a Func<>, chiama IEnumerableinvece le cose LINQ invece di IQueryable, il che significa che intere tabelle vengono estratte e quindi filtrate. Expression<Func<>>è significativamente più veloce, soprattutto se si esegue una query su un database che risiede su un altro server.


Questo vale anche per le query in memoria?
stt106,

@ stt106 Probabilmente no.
mhenry1384,

Questo è vero solo se si enumera l'elenco. Se si utilizza GetEnumerator o foreach non si caricherà l'enumerabile completamente in memoria.
nelsontruran,

1
@ stt106 Quando viene passato alla clausola .Where () di un Elenco <>, Expression <Func <>> viene richiamato da .Compile (), quindi Func <> è quasi certamente più veloce. Vedere riferimentiource.microsoft.com/#System.Core/System/Linq/…
NStuke
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.