11 LINQ
11.1 Was ist LINQ?
LINQ (Language Integrated Query) ist eine Sprachergänzung von .NET, die mit .NET 3.5 eingeführt worden ist. Die Idee, die sich hinter LINQ verbirgt, ist, die Abfrage von Daten aus verschiedenen Datenquellen zu vereinfachen. Zu diesem Zweck stellt LINQ ein Modell zur Verfügung, mit dem einheitlich auf Daten aus verschiedensten Datenquellen zugegriffen werden kann, beispielsweise auf SQL-Datenbanken, auf XML-Dokumente und .NET-Auflistungen. Das Besondere ist dabei, dass Abfragen direkt als Code in C# oder andere .NET-Sprachen eingebunden werden können und nicht nur wie bisher als Zeichenfolge. Infolgedessen muss man also nicht mehr zwangsläufig SQL lernen, um Datenbanken abzufragen, oder XML Query, um Daten aus einem XML-Dokument zu lesen.
Die Syntax von LINQ ähnelt verblüffend den Abfragebefehlen von SQL, und so sind auch in LINQ Sprachelemente wie select, from oder where zu finden. Ein weiterer Vorteil von LINQ ist, dass dieses Abfragemodell als Teil der Sprache kompiliert werden kann und damit von IntelliSense unterstützt wird. Anders als etwa bei SQL-Abfragen, die erst zur Laufzeit ausgeführt werden, können Fehler so viel schneller gefunden werden.
Das folgende Beispiel soll Ihnen einen ersten Eindruck von LINQ vermitteln.
// Beispiel: ..\Kapitel 11\FirstLINQSample
class Program
{
static void Main(string[] args)
{
Person[] persons = {
new Person { Name = "Meier", Age = 34 },
new Person { Name = "Müller", Age = 51 },
new Person { Name = "Schmidt", Age = 30 },
new Person { Name = "Fischer", Age = 25 },
new Person { Name = "Schulz", Age = 67 },
};
var query = from pers in persons
where pers.Age >= 50
select pers;
foreach (var item in query)
Console.WriteLine("{0,-8}{1}", item.Name, item.Age);
Console.ReadLine();
}
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Listing 11.1 Beispielprogramm »FirstLINQSample«
Im Beispiel wird ein Array aus mehreren Personen gebildet, das anschließend in der Weise gefiltert wird, dass nur alle Personen, die 50 Jahre alt sind oder älter, in die Ergebnismenge aufgenommen werden. Zur Bildung der Ergebnismenge wird ein LINQ-Ausdruck verwendet:
var query = from pers in persons
where pers.Age >= 50
select pers;
Listing 11.2 Abfragesyntax
Die von LINQ verwendete Syntax ähnelt der, die Sie vielleicht von SQL her kennen. An dieser Stelle sei bereits angedeutet, dass auch die Formulierung eines LINQ-Ausdrucks mit Erweiterungsmethoden möglich ist und zum gleichen Resultat führt:
var query = persons
.Where(p => p.Age >= 50)
.Select(p => p);
Listing 11.3 Erweiterungsmethodensyntax
Es spielt keine Rolle, woher die Daten in der Liste der Personen stammt: Es könnte sich zum Beispiel auch um die Ergebnismenge einer Datenbankabfrage handeln. LINQ ist in jedem Fall datenquellenneutral.
Die Einführung von LINQ mit C# 3.5 zwang das .NET-Entwicklerteam dazu, die .NET-Sprachen zu ergänzen. Dazu gehören Lambda-Ausdrücke, implizite Typisierung, Objektinitialisierer, anonyme Typen und Erweiterungsmethoden. Diese Sprachfeatures haben wir uns in den vergangenen Kapiteln bereits angesehen. Sie können LINQ-Abfragen in C# mit SQL Server-Datenbanken, XML-Dokumenten, ADO.NET-Datasets schreiben sowie jede Auflistung von Objekten abfragen. Es gibt allerdings dabei eine wichtige Bedingung zu beachten: Die Liste muss das Interface IEnumerable<T> implementieren.
11.1.1 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 deren 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 geht die erneute Ausführung natürlich auch zu Lasten der Leistung.
Ob das Verhalten der verzögerten Ausführung positiv oder eher negativ zu bewerten ist, hängt vom Einzelfall ab. In einer Anwendung, die mehrfach auf die Abfrageresultate zugreifen muss, können Sie mit den Methoden ToArray, ToList oder ToDictionary die Ergebnismenge zwischenspeichern. Keine Angst, Sie haben noch nichts verpasst, denn auf die genannten Methoden werden wir später noch eingehen.
11.1.2 LINQ-Erweiterungsmethoden an einem Beispiel
Das Fundament von LINQ sind die zahlreichen Erweiterungsmethoden, die im Namespace System.Linq definiert sind. Ehe wir uns eingehender mit LINQ beschäftigen, möchte ich Ihnen zeigen, wie eine LINQ-Erweiterungsmethode 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 möglichst flexibel zu sein, wird die Überprüfung in eine andere Methode ausgelagert, die FilterName lauten soll. Der Methode GetShortNames wird neben dem Zeichenfolge-Array auch ein Delegat auf FilterName übergeben.
class Program {
delegate bool FilterHandler(string name);
static void Main(string[] args) {
string[] arr = { "Peter", "Uwe", "Willi", "Udo", "Gernot" };
FilterHandler del = FilterName;
GetShortNames(arr, del);
Console.ReadLine();
}
static void GetShortNames(string[] arr, FilterHandler del) {
foreach (string name in arr)
if (del(name)) Console.WriteLine(name);
}
static bool FilterName(string name) {
if (name.Length < 4) return true;
return false;
}
}
Listing 11.4 Filtern eines Zeichenfolgearrays
So weit funktioniert der Code einwandfrei. Was würden Sie aber machen, wenn Sie in einem anderen Kontext nicht die 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 Filter zu berücksichtigen?
Es geht auch anders, denn dasselbe Ergebnis wie in Listing 11.4 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);
}
}
Listing 11.5 Filtern eines Zeichenfolgearrays mit einem Lambda-Ausdruck
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 einen generischen Delegaten. Schauen wir uns dessen Definition an:
public delegate TResult Func<T, TResult>(T arg)
Der Delegat kann auf eine Methode zeigen, die einen Parameter entgegennimmt. Der generische Typ T beschreibt den Typ des Übergabeparameters, TResult den Typ der Rückgabe.
Im .NET Framework sind noch zahlreiche weitere Func-Delegaten vordefiniert. Damit werden Methoden beschrieben, die nicht nur einen, sondern bis zu 16 Parameter definieren. Eines haben aber alle Func-Definitionen gemeinsam: Der letzte generische Typparameter beschreibt immer den Datentyp der Ergebnismenge.
Vielleicht erinnern Sie sich: Ein Delegat kann auch durch einen Lambda-Ausdruck beschrieben werden. Das haben wir in Listing 11.5 durch die Übergabe von
Func<T, bool> getNames = name => name.Length < 4
genutzt. Der Übergabewert ist hier ein String, das Ergebnis der Operation ein boolescher Wert.
Wichtig ist, dass Sie erkennen, dass die Methode GetShortNames jetzt mit ganz unterschiedlichen Filtern 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.
Rufen wir uns an dieser Stelle noch einmal das einführende LINQ-Beispiel des Listings 11.3 ins Gedächtnis zurück:
var query = persons
.Where(p => p.Age >= 50)
.Select(p => p);
Sieht die Filterung mit GetShortNames in Listing 11.5 nicht bereits sehr ähnlich der Filterung mit der Where-Methode aus?
Es gibt aber noch einen entscheidenden Unterschied: Wir übergeben der Methode GetShortNames die zu sortierende Liste als Argument. Besser wäre es, wir würden die Methode auf das Listenobjekt aufrufen. Dazu muss die Methode als Erweiterungsmethode definiert werden, wobei sich noch die Frage stellt, welche Klassen erweitert werden sollen und welchen Rückgabewert die Methode haben soll. Um die Allgemeingültigkeit der Methode sicherzustellen, legen wir fest, dass die Methode die Klassen erweitern soll, die IEnumerable<T> implementieren. Diese Schnittstelle soll auch gleichzeitig den Rückgabewert beschreiben, um damit zu gewährleisten, dass die Ergebnismenge in einer foreach-Schleife durchlaufen werden kann.
Diese Überlegungen erfordern es, dass der Code in Listing 11.5 an die Erweiterungsmethode GetShortNames angepasst werden muss. Bekanntlich müssen Erweiterungsmethoden in einer statischen Klasse definiert sein. Im folgenden Listing ist daher eine weitere Klasse definiert, die unsere Erweiterungsmethode enthält. Darüber hinaus wird der Bezeichner GetShortNames in Where geändert.
// Beispiel: ..\Kapitel 11\UserDefinedFilter
using System;
using System.Collections.Generic;
using System.Collections;
class Program {
static void Main(string[] args)
{
string[] arr = { "Peter", "Uwe", "Willi", "Udo" };
IEnumerable<string> query = arr.Where(name => name.Length < 4);
foreach (string item in query)
Console.WriteLine(item);
Console.ReadLine();
}
}
static class Extensionmethod {
// Erweiterungsmethode
public static IEnumerable<T> Where<T>(this IEnumerable<T> liste,
Func<T, bool> filter)
{
List<T> result = new List<T>();
foreach (T name in liste)
if (filter(name))
result.Add(name);
return result;
}
}
Listing 11.6 Beispielprogramm »UserDefinedFilter«
Das Resultat zur Laufzeit wird dasselbe wie vorher sein. Allerdings haben wir nun eine Erweiterungsmethode entwickelt, die nicht nur ein String-Array nach einer bestimmten Bedingung filtern kann, sondern jede x-beliebige Liste – vorausgesetzt, sie implementiert das Interface IEnumerable<T>. Tatsächlich funktioniert die LINQ-Erweiterungsmethode Where in derselben Weise. Werfen wir deshalb einen Blick auf die Definition der Methode von LINQ:
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 Delegat, der im ersten generischen Parameter den in der Liste enthaltenen Typ beschreibt. Der zweite Typparameter gibt den Rückgabewert Boolean des Delegaten an.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.