Einführung in LINQ-Abfragen in C#

Eine Abfrage ist ein Ausdruck, der Daten von einer Datenquelle abruft. Verschiedene Datenquellen enthalten verschiedene native Abfragesprachen, beispielsweise SQL für relationale Datenbanken und XQuery für XML. Entwickler*innen müssen für jeden Typ von Datenquelle oder Datenformat, den sie unterstützen müssen, eine neue Abfragesprache erlernen. LINQ vereinfacht diese Situation durch die Bereitstellung eines konsistenten C#-Sprachmodells für verschiedene Arten von Datenquellen und Formaten. In einer LINQ-Abfrage arbeiten Sie immer mit C#-Objekten. Sie verwenden dieselben grundlegenden Codierungsmuster für die Abfrage und Transformation von Daten in XML-Dokumenten, SQL-Datenbanken, .NET-Auflistungen sowie allen anderen Formaten, wenn ein LINQ-Anbieter verfügbar ist.

Drei Teile einer Abfrageoperation

Alle LINQ-Abfrageoperationen bestehen aus drei unterschiedlichen Aktionen:

  1. Abrufen der Datenquelle
  2. Erstellen der Abfrage
  3. Ausführen der Abfrage

Im folgenden Beispiel wird gezeigt, wie die drei Teile einer Abfrageoperation in Quellcode ausgedrückt werden. Das Beispiel verwendet aus praktischen Gründen ein Array von Ganzzahlen als Datenquelle. Dieselben Konzepte gelten jedoch auch für andere Datenquellen. Auf dieses Beispiel wird im Rest dieses Artikels Bezug genommen.

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

Die folgende Abbildung zeigt die vollständige Abfrageoperation. In LINQ unterscheidet sich die Ausführung der Abfrage von der Abfrage selbst. Anders ausgedrückt: Sie rufen keine Daten ab, indem Sie eine Abfragevariable erstellen.

Diagramm des vollständigen LINQ-Abfragevorgangs

Die Datenquelle

Bei der Datenquelle im vorherigen Beispiel handelt es sich um ein Array, das die generische IEnumerable<T>-Schnittstelle unterstützt. Das bedeutet, dass sie mit L abgefragt werden kann. Eine Abfrage wird in einer foreach-Anweisung ausgeführt, und foreach erfordert IEnumerable oder IEnumerable<T>. Typen, die IEnumerable<T> unterstützen oder eine abgeleitete Schnittstelle, wie z.B. der generische Typ IQueryable<T>, werden als abfragbare Typen bezeichnet.

Für abfragbare Typen ist keine Änderung oder besondere Behandlung notwendig, um sie als LINQ-Datenquelle zu verwenden. Wenn die Quelldaten nicht bereits als abfragbarer Typ im Arbeitsspeicher vorhanden sind, muss der LINQ-Anbieter diese als solchen darstellen. Zum Beispiel lädt LINQ to XML ein XML-Dokument in einen abfragbaren XElement-Typ:

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

Mit EntityFramework erstellen Sie eine objektrelationale Zuordnung zwischen C#-Klassen und Ihrem Datenbankschema. Sie schreiben die Abfragen für die Objekte, und zur Laufzeit übernimmt EntityFramework die Kommunikation mit der Datenbank. Im folgenden Beispiel stellt Customers eine bestimmte Tabelle in der Datenbank dar, und der Typ des Abfrageergebnisses, IQueryable<T>, wird von IEnumerable<T> abgeleitet.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

Weitere Informationen zum Erstellen bestimmter Typen von Datenquellen finden Sie in der Dokumentation der verschiedenen LINQ-Anbieter. Die Grundregel ist jedoch einfach: Eine LINQ-Datenquelle ist jedes Objekt, das die generische IEnumerable<T>-Schnittstelle oder eine Schnittstelle unterstützt, die davon erbt (in der Regel IQueryable<T>).

Hinweis

Typen wie ArrayList, die die nicht generische IEnumerable-Schnittstelle unterstützen, können ebenso als LINQ-Datenquelle verwendet werden. Weitere Informationen finden Sie unter Vorgehensweise: Abfragen von ArrayList mit LINQ (C#).

Die Abfrage

Die Abfrage gibt an, welche Informationen aus der Datenquelle oder den Datenquellen abgerufen werden sollen. Optional kann eine Abfrage auch angeben, wie diese Informationen vor der Rückgabe sortiert, gruppiert und strukturiert werden sollen. Eine Abfrage wird in einer Abfragevariablen gespeichert und mit einem Abfrageausdruck initialisiert. Sie verwenden die C#-Abfragesyntax zum Schreiben von Abfragen.

Die Abfrage im vorherigen Beispiel gibt alle geraden Zahlen aus einem Ganzzahlen-Array zurück. Der Abfrageausdruck enthält drei Klauseln: from, where und select. (Wenn Sie mit SQL vertraut sind, ist Ihnen wahrscheinlich aufgefallen, dass die Klauseln im Vergleich zu SQL in umgekehrter Reihenfolge angeordnet sind.) Die from-Klausel gibt die Datenquelle an, die where-Klausel wendet den Filter an, und die select-Klausel gibt den Typ der zurückgegebenen Elemente an. Alle diese Abfrageklauseln werden ausführlich in diesem Abschnitt erläutert. Wichtig ist hier, dass die Abfragevariable selbst in LINQ keine Aktion ausführt und keine Daten zurückgibt. Sie speichert nur die Informationen, die erforderlich sind, um Ergebnisse zu erzeugen, wenn die Abfrage zu einem späteren Zeitpunkt ausgeführt wird. Weitere Informationen zum Erstellen von Abfragen finden Sie unter Übersicht über Standardabfrageoperatoren (C#).

Hinweis

Abfragen können auch unter Verwendung der Methodensyntax ausgedrückt werden. Weitere Informationen finden Sie unter Abfragesyntax und Methodensyntax in LINQ.

Klassifizierung von Standardabfrageoperatoren nach der Art der Ausführung

Die LINQ to Objects-Implementierungen des Standardabfrageoperators werden mit einer von zwei möglichen Arten ausgeführt: direkt oder zurückgestellt. Abfrageoperatoren, die die zurückgestellte Ausführung verwenden, können darüber hinaus in zwei Kategorien unterteilt werden: Streaming und Nicht-Streaming.

Direkt

Sofortige Ausführung bedeutet, dass die Datenquelle gelesen und die Operation ein Mal ausgeführt wird. Alle Standardabfrageoperatoren, die ein skalares Ergebnis zurückgeben, werden sofort ausgeführt. Beispiele für solche Abfragen sind Count, Max, Average und First. Diese Methoden werden ohne explizite foreach-Anweisung ausgeführt, da die Abfrage selbst foreach verwenden muss, um ein Ergebnis zurückzugeben. Diese Abfragen geben einen einzelnen Wert und keine IEnumerable-Auflistung zurück. Sie können erzwingen, dass jede Abfrage sofort ausgeführt wird, indem Sie die Enumerable.ToList- oder Enumerable.ToArray-Methode verwenden. Die sofortige Ausführung ermöglicht die Wiederverwendung von Abfrageergebnissen, aber keine Wiederverwendung der Abfragedeklaration. Die Ergebnisse werden einmal abgerufen und dann für die zukünftige Verwendung gespeichert. Die folgende Abfrage gibt eine Anzahl der geraden Zahlen im Quellarray zurück:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Um die unmittelbare Ausführung einer Abfrage zu erzwingen und ihre Ergebnisse zwischenzuspeichern, können Sie die ToList-Methode oder die ToArray-Methode aufrufen.

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

Sie können die Ausführung auch erzwingen, indem Sie die foreach-Schleife unmittelbar nach dem Abfrageausdruck setzen. Durch Aufrufen von ToList oder ToArray speichern Sie jedoch auch alle Daten in einem einzelnen Auflistungsobjekt zwischen.

Zurückgestellt

Zurückgestellte Ausführung bedeutet, dass der Vorgang nicht zum Zeitpunkt im Code ausgeführt wird, an dem die Abfrage deklariert wird. Der Vorgang erfolgt nur, wenn die Abfragevariable aufgezählt wird, z.B. durch Verwendung einer foreach-Anweisung. Die Ergebnisse der Ausführung der Abfrage hängen vom Inhalt der Datenquelle zum Zeitpunkt der Abfrageausführung ab, nicht vom Zeitpunkt der Abfragedefinition. Wenn die Abfragevariable mehrfach aufgezählt wird, können die Ergebnisse jedes Mal abweichen. Fast alle Standardabfrageoperatoren, deren Rückgabetyp IEnumerable<T> oder IOrderedEnumerable<TElement> ist, werden verzögert ausgeführt. Die verzögerte Ausführung ermöglicht die Wiederverwendung von Abfragen, da die Abfrage die aktualisierten Daten bei jeder Iteration der Abfrageergebnisse aus der Datenquelle abruft. Im folgenden Code ist ein Beispiel für die zurückgestellte Methode dargestellt:

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

In der foreach-Anweisung werden auch die Abfrageergebnisse abgerufen. So ist beispielsweise in der vorherigen Abfrage in der Iterationsvariablen num jeder Wert (jeweils einzeln) der zurückgegebenen Sequenz enthalten.

Da die Abfragevariable selbst nie die Abfrageergebnisse enthält, können Sie sie wiederholt ausführen, um aktualisierte Daten abzurufen. Beispielsweise kann eine separate Anwendung eine Datenbank kontinuierlich aktualisieren. Sie können in Ihrer Anwendung eine Abfrage erstellen, die die neuesten Daten abruft. Sie könnten diese Abfrage in Intervallen ausführen, um aktualisierte Ergebnisse abzurufen.

Abfrageoperatoren, die die zurückgestellte Ausführung verwenden, können zusätzlich als Streaming und Nicht-Streaming klassifiziert werden.

Streaming

Streaming-Operatoren müssen nicht alle Quelldaten lesen, bevor sie Elemente liefern. Zum Zeitpunkt der Ausführung führt ein Streaming-Operator seine Operation auf jedem Quellelement aus, während es gelesen wird, und liefert ggf. die Elemente. Ein Streaming-Operator liest weiterhin Quellelemente, bis ein Ergebniselement erzeugt werden kann. Dies bedeutet, dass mehr als ein Quellelement womöglich gelesen werden kann, um ein Ergebniselement zu erzeugen.

Nicht-Streaming

Nicht-Streaming-Operatoren müssen alle Quelldaten lesen, bevor sie ein Ergebniselement liefern können. Vorgänge wie das Sortieren oder Gruppieren fallen unter diese Kategorie. Zum Zeitpunkt der Ausführung lesen Nicht-Streaming-Operatoren alle Quelldaten, fügen sie in eine Datenstruktur ein, führen den Vorgang aus und liefern die Elemente, die sich ergeben.

Klassifizierungstabelle

In der folgenden Tabelle wird jede Standardabfrageoperator-Methode laut der Ausführungsmethode klassifiziert.

Hinweis

Wenn ein Operator in zwei Spalten gekennzeichnet ist, werden zwei Eingabesequenzen in den Vorgang einbezogen, und jede Sequenz wird unterschiedlich ausgewertet. In diesen Fällen ist es immer die erste Sequenz in der Parameterliste, die verzögert und mit der Nicht-Straming-Methode ausgewertet wird.

Standardabfrageoperator Rückgabetyp Sofortige Ausführung Verzögerte Streamingausführung Zurückgestellte Nicht-Streaming-Ausführung
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average Einzelner numerischer Wert X
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max Einzelner numerischer Wert, TSource oder TResult? X
Min Einzelner numerischer Wert, TSource oder TResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum Einzelner numerischer Wert X
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray TSource[]-Array X
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ to Objects

„LINQ to Objects“ bezieht sich direkt auf die Verwendung von LINQ-Abfragen mit jeder IEnumerable- oder IEnumerable<T>-Sammlung. Sie können LINQ zur Abfrage beliebiger aufzählbarer Auflistungen wie List<T>, Array oder Dictionary<TKey,TValue> verwenden. Die Sammlung kann benutzerdefiniert sein oder ein von einer .NET-API zurückgegebener Typ. Im LINQ-Ansatz verfassen Sie einen deklarativen Code, in dem beschrieben wird, was Sie abrufen möchten. LINQ to Objects bietet eine ideale Einführung in die Programmierung mit LINQ.

LINQ-Abfragen bieten drei wesentliche Vorteile gegenüber herkömmlichen foreach-Schleifen:

  • Sie sind präziser und lesbarer, insbesondere beim Filtern mehrerer Bedingungen.
  • Sie bieten mit minimalem Anwendungscode leistungsstarke Filter-, Sortier- und Gruppierungsfunktionen.
  • Sie können mit geringfügigen oder ohne Änderungen zu anderen Datenquellen portiert werden.

Je komplexer der für die Daten durchzuführende Vorgang, desto größer ist der Vorteil, den Sie durch die Verwendung von LINQ anstelle der herkömmlichen Iterationsverfahren haben.

Speichern der Ergebnisse einer Abfrage im Speicher

Eine Abfrage besteht im Grunde aus einer Reihe von Anweisungen für das Abrufen und Organisieren von Daten. Abfragen werden verzögert ausgeführt, da jedes nachfolgende Element im Ergebnis angefordert wird. Wenn Sie foreach zum Durchlaufen der Ergebnisse verwenden, werden Elemente so zurückgegeben, wie auf sie zugegriffen wurde. Rufen Sie einfach eine der folgenden Methoden für die Abfragevariable auf, um eine Abfrage auszuwerten und ihre Ergebnisse ohne das Ausführen einer foreach-Schleife zu speichern:

Sie sollten die zurückgegebenen Auflistungsobjekte beim Speichern der Abfrageergebnisse einer neuen Variable zuweisen, wie im folgenden Beispiel gezeigt wird:

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

Weitere Informationen