Was ist der Unterschied zwischen .ToList (), .AsEnumerable (), AsQueryable ()?


182

Ich kenne einige Unterschiede zwischen LINQ zu Entitäten und LINQ zu Objekten, die der erste IQueryableund der zweite implementiert, IEnumerableund mein Fragenbereich liegt innerhalb von EF 5.

Meine Frage ist, was ist der technische Unterschied (die technischen Unterschiede) dieser drei Methoden? Ich sehe, dass in vielen Situationen alle funktionieren. Ich sehe auch Kombinationen davon gerne .ToList().AsQueryable().

  1. Was bedeuten diese Methoden genau?

  2. Gibt es ein Leistungsproblem oder etwas, das dazu führen würde, dass eines über das andere verwendet wird?

  3. Warum sollte man zum Beispiel .ToList().AsQueryable()statt verwenden .AsQueryable()?


Antworten:


354

Dazu gibt es viel zu sagen. Lassen Sie mich auf AsEnumerableund konzentrieren AsQueryableund ToList()auf dem Weg erwähnen .

Was machen diese Methoden?

AsEnumerableund AsQueryablegegossen oder konvertiert zu IEnumerableoder IQueryable. Ich sage Besetzung oder Konvertierung mit einem Grund:

  • Wenn das Quellobjekt bereits den Ziel - Schnittstelle implementiert, wird das Quellobjekt selbst zurückgegeben , sondern warf auf die Zieloberfläche. Mit anderen Worten: Der Typ wird nicht geändert, der Typ zur Kompilierungszeit jedoch.

  • Wenn das Quellobjekt die Zielschnittstelle nicht implementiert, wird das Quellobjekt in ein Objekt konvertiert , das die Zielschnittstelle implementiert. Daher werden sowohl der Typ als auch der Typ zur Kompilierungszeit geändert.

Lassen Sie mich dies anhand einiger Beispiele zeigen. Ich habe diese kleine Methode, die den Typ der Kompilierungszeit und den tatsächlichen Typ eines Objekts angibt (mit freundlicher Genehmigung von Jon Skeet ):

void ReportTypeProperties<T>(T obj)
{
    Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
    Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}

Versuchen wir es mit einer beliebigen Linq-to-SQL-Methode Table<T>, die Folgendes implementiert IQueryable:

ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());

Das Ergebnis:

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

Sie sehen, dass die Tabellenklasse selbst immer zurückgegeben wird, ihre Darstellung sich jedoch ändert.

Nun ein Objekt, das implementiert IEnumerable, nicht IQueryable:

var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());

Die Ergebnisse:

Compile-time type: Int32[]
Actual type: Int32[]

Compile-time type: IEnumerable`1
Actual type: Int32[]

Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1

Da ist es. AsQueryable()hat das Array in ein Array konvertiert EnumerableQuery, das "eine IEnumerable<T>Sammlung als IQueryable<T>Datenquelle darstellt". (MSDN).

Was ist der Nutzen?

AsEnumerablewird häufig verwendet, um von einer IQueryableImplementierung zu LINQ zu Objekten (L2O) zu wechseln , hauptsächlich weil ersteres keine Funktionen unterstützt, über die L2O verfügt. Weitere Informationen finden Sie unter Welche Auswirkungen hat AsEnumerable () auf eine LINQ-Entität? .

In einer Entity Framework-Abfrage können wir beispielsweise nur eine begrenzte Anzahl von Methoden verwenden. Wenn wir zum Beispiel eine unserer eigenen Methoden in einer Abfrage verwenden müssen, schreiben wir normalerweise so etwas wie

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => MySuperSmartMethod(x))

ToList - was ein IEnumerable<T>in ein umwandelt List<T>- wird häufig auch für diesen Zweck verwendet. Der Vorteil der Verwendung von AsEnumerablevs. ToListbesteht darin, dass AsEnumerabledie Abfrage nicht ausgeführt wird. AsEnumerableBewahrt die verzögerte Ausführung und erstellt keine häufig nutzlose Zwischenliste.

Wenn andererseits eine erzwungene Ausführung einer LINQ-Abfrage gewünscht wird, ToListkann dies eine Möglichkeit sein.

AsQueryablekann verwendet werden, um eine aufzählbare Sammlung Ausdrücke in LINQ-Anweisungen akzeptieren zu lassen. Weitere Informationen finden Sie hier: Muss ich AsQueryable () wirklich für die Sammlung verwenden? .

Hinweis zum Drogenmissbrauch!

AsEnumerablewirkt wie eine Droge. Es ist eine schnelle Lösung, aber kostenpflichtig und behebt das zugrunde liegende Problem nicht.

In vielen Antworten zum Stapelüberlauf sehe ich Leute, die sich bewerben AsEnumerable, um fast jedes Problem mit nicht unterstützten Methoden in LINQ-Ausdrücken zu beheben. Der Preis ist aber nicht immer klar. Wenn Sie dies beispielsweise tun:

context.MyLongWideTable // A table with many records and columns
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate })

... alles wird ordentlich in eine SQL-Anweisung übersetzt, die filter ( Where) und projects ( Select). Das heißt, sowohl die Länge als auch die Breite der SQL-Ergebnismenge werden reduziert.

Angenommen, Benutzer möchten nur den Datumsteil von sehen CreateDate. In Entity Framework werden Sie schnell feststellen, dass ...

.Select(x => new { x.Name, x.CreateDate.Date })

... wird nicht unterstützt (zum Zeitpunkt des Schreibens). Ah, zum Glück gibt es die AsEnumerableLösung:

context.MyLongWideTable.AsEnumerable()
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate.Date })

Sicher, es läuft wahrscheinlich. Es zieht jedoch die gesamte Tabelle in den Speicher und wendet dann den Filter und die Projektionen an. Nun, die meisten Leute sind klug genug, um das Whereerste zu tun :

context.MyLongWideTable
       .Where(x => x.Type == "type").AsEnumerable()
       .Select(x => new { x.Name, x.CreateDate.Date })

Trotzdem werden zuerst alle Spalten abgerufen und die Projektion erfolgt im Speicher.

Die eigentliche Lösung ist:

context.MyLongWideTable
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })

(Aber das erfordert nur ein bisschen mehr Wissen ...)

Was machen diese Methoden NICHT?

Stellen Sie die IQueryable-Funktionen wieder her

Nun eine wichtige Einschränkung. Wenn Sie das tun

context.Observations.AsEnumerable()
                    .AsQueryable()

Sie erhalten das Quellobjekt, das als dargestellt wird IQueryable. (Weil beide Methoden nur umwandeln und nicht konvertieren).

Aber wenn du es tust

context.Observations.AsEnumerable().Select(x => x)
                    .AsQueryable()

Was wird das Ergebnis sein?

Das Selectproduziert a WhereSelectEnumerableIterator. Dies ist eine interne .NET - Klasse, die implementiert IEnumerable, nichtIQueryable . Es wurde also eine Konvertierung in einen anderen Typ durchgeführt, und die nachfolgende AsQueryablekann die ursprüngliche Quelle nie mehr zurückgeben.

Die Folge davon ist , dass die Verwendung AsQueryableist nicht ein Weg , um auf magische Weise inject ein Abfrage - Provider mit seinem spezifischen Funktionen in eine zählbaren. Angenommen, Sie tun es

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => x.ToString())
                   .AsQueryable()
                   .Where(...)

Die where-Bedingung wird niemals in SQL übersetzt. AsEnumerable()gefolgt von LINQ-Anweisungen wird die Verbindung zum Entity Framework-Abfrageanbieter endgültig unterbrochen.

Ich zeige dieses Beispiel absichtlich, weil ich hier Fragen gesehen habe, bei denen beispielsweise versucht wird, IncludeFunktionen durch Aufrufen in eine Sammlung einzufügen AsQueryable. Es wird kompiliert und ausgeführt, tut aber nichts, da das zugrunde liegende Objekt keine IncludeImplementierung mehr hat.

Ausführen

Sowohl AsQueryableund AsEnumerablenicht ausführen (oder enumerate ) das Quellobjekt. Sie ändern nur ihren Typ oder ihre Darstellung. Beide beteiligten Schnittstellen IQueryableund IEnumerablesind nichts anderes als "eine Aufzählung, die darauf wartet, passiert zu werden". Sie werden nicht ausgeführt, bevor sie dazu gezwungen werden, beispielsweise wie oben erwähnt, durch Aufrufen ToList().

Das bedeutet, dass beim Ausführen eines IEnumerabledurch Aufrufen AsEnumerableeines IQueryableObjekts erhaltenen Objekts der Basiswert ausgeführt wird IQueryable. Eine nachfolgende Ausführung des IEnumerableTestaments führt das erneut aus IQueryable. Welches kann sehr teuer sein.

Spezifische Implementierungen

Bisher ging es nur um die Queryable.AsQueryableund Enumerable.AsEnumerableErweiterungsmethoden. Aber natürlich kann jeder Instanzmethoden oder Erweiterungsmethoden mit denselben Namen (und Funktionen) schreiben.

In der Tat ist ein häufiges Beispiel für eine bestimmte AsEnumerableErweiterungsmethode DataTableExtensions.AsEnumerable. DataTableimplementiert nicht IQueryableoder IEnumerable, daher gelten die regulären Erweiterungsmethoden nicht.


Vielen Dank für die Antwort . Können Sie uns bitte Ihre Antwort auf die dritte Frage von OP - 3 mitteilen? Warum sollte man beispielsweise .ToList (). AsQueryable () anstelle von .AsQueryable () verwenden? ?
kgzdev

@ikram Ich kann mir nichts vorstellen, wo das nützlich wäre. Wie ich bereits erklärt habe, AsQueryable()basiert die Bewerbung häufig auf falschen Vorstellungen. Aber ich lasse es für eine Weile in meinem Hinterkopf köcheln und sehe, ob ich diese Frage noch ausführlicher behandeln kann.
Gert Arnold

1
Gute Antwort. Könnten Sie bitte genau angeben, was passiert, wenn die IEnumerable, die durch Aufrufen von AsEnumerable () auf einer IQueryable erhalten wird, mehrmals aufgelistet wird? Wird die Abfrage mehrmals ausgeführt oder werden die bereits aus der Datenbank in den Speicher geladenen Daten wiederverwendet?
Antoninod

@antoninod Gute Idee. Getan.
Gert Arnold

46

Auflisten()

  • Führen Sie die Abfrage sofort aus

AsEnumerable ()

  • faul (führen Sie die Abfrage später aus)
  • Parameter: Func<TSource, bool>
  • Laden Sie JEDEN Datensatz in den Anwendungsspeicher und behandeln / filtern Sie sie dann. (zB Wo / Nehmen / Überspringen wird * aus Tabelle1 in den Speicher ausgewählt und dann die ersten X-Elemente ausgewählt) (In diesem Fall: Linq-to-SQL + Linq-to-Object)

AsQueryable ()

  • faul (führen Sie die Abfrage später aus)
  • Parameter: Expression<Func<TSource, bool>>
  • Konvertieren Sie Expression in T-SQL (mit dem jeweiligen Anbieter), fragen Sie remote ab und laden Sie das Ergebnis in Ihren Anwendungsspeicher.
  • Aus diesem Grund erbt DbSet (in Entity Framework) auch IQueryable, um die effiziente Abfrage zu erhalten.
  • Laden Sie nicht jeden Datensatz, z. B. wenn Take (5), werden im Hintergrund ausgewählte Top 5 * SQL generiert. Dies bedeutet, dass dieser Typ für die SQL-Datenbank benutzerfreundlicher ist. Aus diesem Grund weist dieser Typ normalerweise eine höhere Leistung auf und wird beim Umgang mit einer Datenbank empfohlen.
  • Funktioniert also AsQueryable()normalerweise viel schneller als AsEnumerable()beim erstmaligen Generieren von T-SQL, das alle Ihre Where-Bedingungen in Ihrem Linq enthält.

14

ToList () ist alles im Speicher und dann werden Sie daran arbeiten. Daher wird ToList (). where (Filter anwenden) lokal ausgeführt. AsQueryable () führt alles remote aus, dh ein Filter wird zur Anwendung an die Datenbank gesendet. Queryable macht nichts, bis Sie es ausführen. ToList wird jedoch sofort ausgeführt.

Schauen Sie sich auch diese Antwort an. Warum sollten Sie AsQueryable () anstelle von List () verwenden? .

BEARBEITEN: In Ihrem Fall ist jede nachfolgende Operation lokal, einschließlich AsQueryable (), sobald Sie ToList () ausführen. Sie können nicht zu Remote wechseln, sobald Sie mit der lokalen Ausführung beginnen. Hoffe das macht es ein bisschen klarer.


2
"AsQueryable () führt alles remote aus" Nur wenn die Aufzählung bereits abfragbar ist. Ansonsten ist dies nicht möglich, und alles , was noch vor Ort läuft. Die Frage hat ".... ToList (). AsQueryable ()", was in Ihrer Antwort, IMO, eine Klarstellung gebrauchen könnte.

2

Beim folgenden Code ist eine schlechte Leistung aufgetreten.

void DoSomething<T>(IEnumerable<T> objects){
    var single = objects.First(); //load everything into memory before .First()
    ...
}

Behoben mit

void DoSomething<T>(IEnumerable<T> objects){
    T single;
    if (objects is IQueryable<T>)
        single = objects.AsQueryable().First(); // SELECT TOP (1) ... is used
    else
        single = objects.First();

}

Bleiben Sie für ein IQueryable nach Möglichkeit in IQueryable und versuchen Sie, nicht wie IEnumerable verwendet zu werden.

Update . Dank Gert Arnold kann es in einem Ausdruck weiter vereinfacht werden .

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.