C'è molto da dire al riguardo. Mi permetta di concentrarsi su AsEnumerable
e AsQueryable
e menzione ToList()
lungo la strada.
Cosa fanno questi metodi?
AsEnumerable
e AsQueryable
cast o convertire rispettivamente a IEnumerable
o IQueryable
. Dico cast o convertire con un motivo:
Quando l'oggetto di origine implementa già l'interfaccia di destinazione, l'oggetto di origine stesso viene restituito ma trasmesso all'interfaccia di destinazione. In altre parole: il tipo non è cambiato, ma il tipo di compilazione è.
Quando l'oggetto di origine non implementa l'interfaccia di destinazione, l'oggetto di origine viene convertito in un oggetto che implementa l'interfaccia di destinazione. Quindi vengono cambiati sia il tipo che il tipo di tempo di compilazione.
Lascia che lo mostri con alcuni esempi. Ho questo piccolo metodo che riporta il tipo di tempo di compilazione e il tipo effettivo di un oggetto (per gentile concessione di Jon Skeet ):
void ReportTypeProperties<T>(T obj)
{
Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}
Proviamo un linq-to-sql arbitrario Table<T>
, che implementa IQueryable
:
ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());
Il risultato:
Compile-time type: Table`1
Actual type: Table`1
Compile-time type: IEnumerable`1
Actual type: Table`1
Compile-time type: IQueryable`1
Actual type: Table`1
Si vede che la stessa classe di tabella viene sempre restituita, ma la sua rappresentazione cambia.
Ora un oggetto che implementa IEnumerable
, non IQueryable
:
var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());
I risultati:
Compile-time type: Int32[]
Actual type: Int32[]
Compile-time type: IEnumerable`1
Actual type: Int32[]
Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1
Eccolo. AsQueryable()
ha convertito l'array in un EnumerableQuery
, che "rappresenta una IEnumerable<T>
raccolta come IQueryable<T>
origine dati". (MSDN).
Come si usa?
AsEnumerable
viene spesso utilizzato per passare da qualsiasi IQueryable
implementazione a LINQ a oggetti (L2O), principalmente perché il primo non supporta le funzioni di L2O. Per maggiori dettagli, vedi Qual è l'effetto di AsEnumerable () su un'entità LINQ? .
Ad esempio, in una query Entity Framework possiamo usare solo un numero limitato di metodi. Quindi, se, ad esempio, dobbiamo usare uno dei nostri metodi in una query, in genere scriveremmo qualcosa del genere
var query = context.Observations.Select(o => o.Id)
.AsEnumerable().Select(x => MySuperSmartMethod(x))
ToList
- che converte an IEnumerable<T>
in a List<T>
- viene spesso utilizzato anche per questo scopo. Il vantaggio di usare AsEnumerable
vs. ToList
è che AsEnumerable
non esegue la query. AsEnumerable
conserva l'esecuzione differita e non crea un elenco intermedio spesso inutile.
D'altra parte, quando si desidera l'esecuzione forzata di una query LINQ, ToList
può essere un modo per farlo.
AsQueryable
può essere utilizzato per fare in modo che una raccolta enumerabile accetti espressioni nelle istruzioni LINQ. Vedi qui per maggiori dettagli: Ho davvero bisogno di usare AsQueryable () sulla collezione? .
Nota sull'abuso di sostanze!
AsEnumerable
funziona come una droga. È una soluzione rapida, ma a un costo e non affronta il problema di fondo.
In molte risposte Stack Overflow, vedo persone che chiedono AsEnumerable
di risolvere qualsiasi problema con metodi non supportati nelle espressioni LINQ. Ma il prezzo non è sempre chiaro. Ad esempio, se lo fai:
context.MyLongWideTable // A table with many records and columns
.Where(x => x.Type == "type")
.Select(x => new { x.Name, x.CreateDate })
... tutto viene tradotto in modo ordinato in un'istruzione SQL che filter ( Where
) e projects ( Select
). Cioè, sia la lunghezza che la larghezza, rispettivamente, del set di risultati SQL sono ridotte.
Supponiamo ora che gli utenti vogliano vedere solo la data di CreateDate
. In Entity Framework scoprirai rapidamente che ...
.Select(x => new { x.Name, x.CreateDate.Date })
... non è supportato (al momento della scrittura). Ah, per fortuna c'è la AsEnumerable
soluzione:
context.MyLongWideTable.AsEnumerable()
.Where(x => x.Type == "type")
.Select(x => new { x.Name, x.CreateDate.Date })
Certo, funziona, probabilmente. Ma tira in memoria l'intera tabella e quindi applica il filtro e le proiezioni. Bene, molte persone sono abbastanza intelligenti da fare il Where
primo:
context.MyLongWideTable
.Where(x => x.Type == "type").AsEnumerable()
.Select(x => new { x.Name, x.CreateDate.Date })
Ma tutte le colonne vengono comunque recuperate per prime e la proiezione viene eseguita in memoria.
La vera soluzione è:
context.MyLongWideTable
.Where(x => x.Type == "type")
.Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })
(Ma ciò richiede solo un po 'più di conoscenza ...)
Cosa NON fanno questi metodi?
Ripristina funzionalità IQueryable
Ora un avvertimento importante. Quando lo fai
context.Observations.AsEnumerable()
.AsQueryable()
finirai con l'oggetto sorgente rappresentato come IQueryable
. (Poiché entrambi i metodi eseguono solo il cast e non convertono).
Ma quando lo fai
context.Observations.AsEnumerable().Select(x => x)
.AsQueryable()
quale sarà il risultato?
Il Select
produce a WhereSelectEnumerableIterator
. Questa è una classe .Net interna che implementa IEnumerable
, nonIQueryable
. Quindi è avvenuta una conversione in un altro tipo e il successivo AsQueryable
non può più restituire la fonte originale.
Ciò implica che l'utilizzo nonAsQueryable
è un modo per iniettare magicamente un provider di query con le sue caratteristiche specifiche in un elenco. Supponi di si
var query = context.Observations.Select(o => o.Id)
.AsEnumerable().Select(x => x.ToString())
.AsQueryable()
.Where(...)
La condizione in cui non verrà mai tradotta in SQL. AsEnumerable()
seguito dalle istruzioni LINQ interrompe definitivamente la connessione con il provider di query del framework di entità.
Mostro deliberatamente questo esempio perché ho visto qui delle domande in cui le persone, ad esempio, cercano di "iniettare" le Include
capacità in una raccolta chiamando AsQueryable
. Si compila ed esegue, ma non fa nulla perché l'oggetto sottostante non ha Include
più un'implementazione.
Eseguire
Entrambi AsQueryable
e AsEnumerable
non eseguono (o enumerano ) l'oggetto di origine. Cambiano solo il loro tipo o rappresentazione. Entrambe le interfacce coinvolte, IQueryable
e IEnumerable
, non sono altro che "un elenco in attesa di accadere". Non vengono eseguiti prima di essere costretti a farlo, ad esempio, come menzionato sopra, chiamando ToList()
.
Ciò significa che eseguendo un IEnumerable
ottenuto chiamando AsEnumerable
un IQueryable
oggetto, verrà eseguito il sottostante IQueryable
. Una successiva esecuzione del IEnumerable
test eseguirà nuovamente il IQueryable
. Che può essere molto costoso.
Implementazioni specifiche
Finora, si trattava solo dei metodi di estensione Queryable.AsQueryable
e Enumerable.AsEnumerable
. Ma ovviamente chiunque può scrivere metodi di istanza o metodi di estensione con gli stessi nomi (e funzioni).
In effetti, un esempio comune di un AsEnumerable
metodo di estensione specifico è DataTableExtensions.AsEnumerable
. DataTable
non implementa IQueryable
o IEnumerable
, quindi i metodi di estensione regolari non si applicano.