10 LINQ to Objects
LINQ (Language Integrated Query) ist eine Sprachergänzung von .NET, die mit dem .NET Framework 3.5 und Visual Studio 2008 eingeführt worden ist. LINQ ermöglicht es, über ein neues Abstraktionsmodell Daten abzufragen. Dabei spielt es keine Rolle, welcher Natur die Daten sind: Es kann sich um ein XML-Dokument handeln oder um eine Datenbanktabelle, um eine Excel-Tabelle oder auch um eine Auflistung von herkömmlichen Objekten. Diese Liste könnte beliebig fortgesetzt werden.
10.1 Einführung in LINQ 

Stellen Sie sich LINQ als eine Erweiterung vor, die dazu dient, den Zugriff auf Daten zu vereinheitlichen und zu vereinfachen. Wegen der unterschiedlichen Datenquellen wurden mehrere LINQ-Implementierungen entwickelt, die als Bestandteil des .NET Frameworks als Provider bereitgestellt werden:
- LINQ to Objects wird durch den Namespace System.Linq zur Verfügung gestellt und bildet das Fundament aller LINQ-Abfragen. Mit LINQ to Objects lassen sich Auflistungen und Objekte manipulieren, die untereinander in Beziehung gesetzt werden können. Dabei beschränkt sich der Einsatz nicht nur auf benutzerdefinierte Daten.
- LINQ to XML bietet eine Programmierschnittstelle für XML im Arbeitsspeicher, die das in .NET sprachintegrierte Abfrage-Framework nutzt.
- LINQ to SQL ist Microsofts LINQ-Provider für das hauseigene Datenbanksystem SQL Server 2005 bzw. 2008.
- LINQ to ADO.NET besteht aus zwei separaten Technologien: LINQ to DataSet und LINQ to SQL. LINQ to DataSet ermöglicht umfangreichere, optimierte Abfragen der Daten aus einem DataSet, und mit LINQ to SQL können Sie SQL-Server-Datenbankschemas direkt abfragen.
Jeder dieser Provider verfügt über eigene Klassen, um den spezifischen Bedürfnissen der entsprechenden Datenquelle zu entsprechen. Da LINQ eine offene Architektur ist, kann man davon ausgehen, dass das .NET Framework in Zukunft um weitere Provider ergänzt werden wird. In diesem Kapitel werden wir uns ausschließlich den Grundlagen von LINQ to Objects widmen.
Im Folgenden möchte ich Ihnen eine typische LINQ-Abfrage vorstellen, damit Sie einen ungefähren Begriff davon bekommen, was Sie in diesem Kapitel erwartet.
var pers = from p in personen where p.Alter > 30 select new { p.Name, p.Alter };
Gleichwertig ist auch die folgende Formulierung:
var pers = personen .Where( p => p.Alter > 30 ) .Select( p => new {p.Name, p.Alter });
Diese Abfrage ruft aus einer Liste die Personen ab, die älter als 30 Jahre sind, und gibt in der Ergebnisliste den Namen und das Alter der gefundenen Personen an. Es spielt keine Rolle, woher die Daten in der Liste personen stammen: Es könnte sich beispielsweise um ein Array von Person-Objekten oder um die Ergebnismenge einer Datenbankabfrage handeln. Die Abfragesyntax ist in jedem Fall datenquellenneutral.
Die von LINQ verwendete Syntax ähnelt der, die Sie vielleicht von SQL her kennen. Die Einführung von LINQ mit C# 3.0 zwang das .NET-Entwicklerteam dazu, die .NET-Sprachen zu ergänzen. Dazu gehören Lambda-Ausdrücke, Typinferenz, Objektinitialisierer, anonyme Typen und Erweiterungsmethoden. Diese Sprachfeatures wurden in den vergangenen Kapiteln bereits behandelt.
10.1.1 Grundlagen der LINQ-Erweiterungsmethoden 

Ehe wir uns intensiver mit LINQ beschäftigen möchte ich Ihnen zeigen, wie ein Abfrageausdruck in der Form, wie er oben gezeigt wurde, zustande kommt. Dazu erzeugen wir ein String-Array mit mehreren Vornamen. Unser Ziel soll es sein, nur die Namen auszugeben, die einer bestimmten Maximallänge entsprechen. Für die Ausgabe soll eine Methode namens GetShortNames implementiert werden. Normalerweise würde die Überprüfung der Länge der einzelnen Namen in dieser Methode codiert. Um aber möglichst flexibel zu sein, wird die Überprüfung in eine andere Methode ausgelagert, die TestName lauten soll. Der Methode GetShortNames wird neben dem Zeichenfolge-Array auch ein Delegate auf TestName übergeben.
class Program { delegate bool MyDelegate(string name); static void Main(string[] args) { string[] arr = { "Peter", "Uwe", "Willi", "Udo" }; MyDelegate del = TestName; GetShortNames(arr, del); Console.ReadLine(); } static void GetShortNames(string[] arr, MyDelegate del) { foreach (string name in arr) if (del(name)) Console.WriteLine(name); } static bool TestName(string name) { if (name.Length < 4) return true; return false; } }
So weit funktioniert dieser Code einwandfrei. Was würden Sie aber machen, wenn Sie in einem anderen Kontext nicht alle Namen selektieren wollen, die weniger als vier Buchstaben aufweisen, sondern beispielsweise mehr als sieben? Richtig, Sie würden eine weitere Methode bereitstellen, die genau das leistet. Und nun eine ganz gemeine Frage: Wie viele unterschiedliche Methoden wären Sie bereit zu implementieren, um möglichst viele, wenn nicht sogar alle denkbaren Filter zu berücksichtigen?
Aber es geht auch anders, denn dasselbe Ergebnis erreichen Sie, wenn Sie einen Lambda-Ausdruck benutzen. Der Code zur Überprüfung der Zeichenfolgelänge wird hierbei direkt in der Parameterliste von GetShortNames aufgeführt.
class Program { static void Main(string[] args) { string[] arr = { "Peter", "Uwe", "Willi", "Udo" }; GetShortNames(arr, name => name.Length < 4); Console.ReadLine(); } static void GetShortNames<T>(T[] names, Func<T, bool> getNames) { foreach (T name in names) if (getNames(name)) Console.WriteLine(name); } }
Beachten Sie bitte den zweiten Parameter der Methode GetShortNames. Dessen Typ Func<T, bool> wird durch das .NET Framework bereitgestellt. Dabei handelt es sich um ein Delegate. Schauen wir uns die Definition dieses Delegates an:
public delegate TResult Func<T, TResult>(T arg)
Dieses generische Delegate kann auf eine Methode zeigen, die einen Parameter entgegennimmt. Der generische Typ T beschreibt den Typ des Übergabeparameters, TResult den Rückgabetyp. Das Besondere an diesem Delegate ist, dass ihm ein Lambda-Ausdruck zugewiesen werden kann:
Func<T, bool> getNames = name => name.Length < 4
Das Ergebnis der Operation ist ein boolescher Wert. Delegates dieser Art (wie hier Func) können Sie natürlich auch selbst definieren.
Wichtig ist, dass Sie erkennen, dass die Methode GetShortNames jetzt mit ganz unterschiedlichen Prädikaten aufgerufen werden kann. Vielleicht wollen Sie beim nächsten Mal alle Namen selektieren, die mit dem Buchstaben »H« beginnen. Kein Problem: Sie brauchen dazu keine weitere Methode zu schreiben und können die vorliegende benutzen, da der Lambda-Ausdruck in der Methode GetShortNames zur Auswertung herangezogen wird (vielleicht erinnern Sie sich, ein Lambda-Ausdruck ist im Grunde nichts anderes als eine anonyme Methode). Nur über die Namensgebung der Methode sollten Sie sich noch einmal Gedanken machen …
Rufen wir uns an dieser Stelle noch einmal das einführende LINQ-Beispiel ins Gedächtnis zurück:
var pers = personen .Where( p => p.Alter > 30 ) .Select( p => new {p.Name, p.Alter});
Widmen Sie Ihre Aufmerksamkeit hier dem Ausdruck Where. Hier handelt es sich um eine Erweiterungsmethode. Sie beschreibt die Operation, aufgrund derer Daten aus der Liste personen selektiert werden sollen. Was der Methode als Parameter übergeben wird, ist eine Operation, die einen booleschen Wert als Resultat liefert. Das erinnert sehr stark an unsere Methode GetShortNames. Sehen wir uns die Signatur von Where an: Sie ähnelt der unserer Methode GetShortNames:
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Der erste Parameter kennzeichnet Where als Erweiterungsmethode für alle Typen, die die Schnittstelle IEnumerable<T> implementieren. Der zweite Parameter ist ein Delegate, das im ersten generischen Parameter den Typ der Datenquelle beschreibt. Der zweite Typparameter kennzeichnet den Rückgabewert Boolean.
10.1.2 Verzögerte Ausführung 

LINQ-Abfragen haben ein besonderes Charakteristikum. Sie werden nämlich nicht sofort ausgeführt, sondern erst, wenn die Ergebnismenge benötigt wird. Das könnte beispielsweise eine foreach-Schleife sein, innerhalb der die Abfrageresultate verarbeitet werden.
Greifen Sie wiederholt auf die Ergebnismenge zu, wird die Abfrage jedes Mal erneut ausgeführt – die Ergebnismenge wird also nicht gecacht. Hat sich in der Zwischenzeit die Datenquelle geändert, erhalten Sie die aktualisierten Daten und profitieren von diesem Verhalten. Andererseits kostet die erneute Ausführung der Abfrage auch Performance.
Ob das Verhalten der verzögerten Ausführung als positiv oder eher als negativ zu bewerten ist, hängt vom Einzelfall ab. In einer Anwendung, die mehrfach die Daten abfragen muss, können Sie mit den Methoden ToArray, ToList oder ToDictionary die Ergebnismenge zwischenspeichern – zumindest solange gewährleistet ist, dass sich die Datenquelle nicht zu häufig ändert. Alle drei Methoden werden auf dem IEnumerable<TSource>-Objekt aufgerufen, oder mit anderen Worten, auf der Ergebnismenge.