Capisco lambda e l' Func
e Action
delegati. Ma le espressioni mi sconcertano.
In quali circostanze Expression<Func<T>>
useresti un vecchio piuttosto che un semplice Func<T>
?
Capisco lambda e l' Func
e Action
delegati. Ma le espressioni mi sconcertano.
In quali circostanze Expression<Func<T>>
useresti un vecchio piuttosto che un semplice Func<T>
?
Risposte:
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 delegate
che è 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:
Mentre entrambi sembrano uguali in fase di compilazione, ciò che genera il compilatore è totalmente diverso .
Expression
contiene le meta-informazioni su un determinato delegato.
Expression<Func<...>>
anziché solo Func<...>
.
(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
tale espressione è un ExpressionTree, i rami sono creati per l'istruzione If.
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 Expression
invece di Func
, finendo qui.
Un'espressione trasforma semplicemente un delegato in un dato su se stesso. Quindi a => a + 1
diventa 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 Expression
e un Func
non è adeguato. Func
non 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. Expression
d'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. Func
non 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 Expression
a 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.
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)
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;Func<T>
;ExpressionVisitor
;Func<T>
;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.
C'è una spiegazione più filosofica al riguardo dal libro di Krzysztof Cwalina ( Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries );
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.
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 Func
in Expression
potenziatori il driver per analizzare Func
e trasformarlo in una query Sql / MongoDb / other.
Expression
Func/Action
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
/ out
args 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 .
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.
Il motivo principale è quando non si desidera eseguire direttamente il codice, ma piuttosto si desidera ispezionarlo. Questo può essere per qualsiasi numero di motivi:
Expression
può 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.
Non vedo ancora risposte che menzionino la performance. Passare Func<>
s in Where()
o Count()
è male. Davvero male. Se usi a Func<>
, chiama IEnumerable
invece 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.