8.3 LINQ to Objects 

Visual C# 3.0 ist im Vergleich zu Visual C# 2.0 um zahlreiche neue Sprachfeatures erweitert worden, wie Sie in den letzten Kapiteln gesehen haben. Lambda-Ausdrücke, Erweiterungsmethoden, Typinferenz und anonyme Typen mussten in die .NET-Sprachen integriert werden, um LINQ zu unterstützen und zu dem zu machen, was es ist: eine Architektur, die es gestattet, mit SQL-ähnlicher Syntax Datenquellen abzufragen.
Darüber hinaus wurde die Liste der C#-Schlüsselwörter um einige ergänzt, die an dieser Stelle erwähnt werden sollten. Wie Sie mit ihnen umgehen, werden Sie im weiteren Verlauf des Kapitels erfahren.
|
from |
where |
into |
orderby |
|
select |
group |
join |
let |
8.3.1 Musterdaten 

Wir werden uns in diesem Abschnitt auf LINQ to Objects konzentrieren und uns weniger mit den spezifischen LINQ-Implementierungen befassen. Dabei werden wir die wichtigsten Klassen und Operatoren untersuchen.
Ehe wir uns mit LINQ beschäftigen, müssen wir uns eine angemessene Datenquelle beschaffen. Die meisten Beispiele in diesem Kapitel arbeiten mit den Daten, die von einer Klassenbibliothek bereitgestellt werden. Sie finden das Projekt auf der Buch-DVD unter \Beispiele\Kapitel 8\Musterdaten.
In der Anwendung sind die vier Klassen Customer, Product, Order und Service sowie die Enumeration Cities definiert. Bis auf Service sind alle sehr einfach gehalten.
public class Order { public int OrderID; public int ProductID; public int Quantity; public bool Shipped; } public class Customer { public string Name; public Cities City; public Order[] Orders; } public class Product { public int ProductID; public string ProductName; public double Price; } public enum Cities { Aachen, Bonn, Köln }
In der Klasse Service werden drei Arrays definiert, die mehrere Produkte, Kunden und Bestellungen beschreiben. Beachten Sie bitte, dass die einzelnen Bestellungen den Kunden direkt in einem Feld zugeordnet werden. Zudem sind in Service drei Methoden implementiert, die als Datenlieferant entweder die Liste der Kunden, der Bestellungen oder der Produkte zurückliefern. Sämtliche Klassenmitglieder sind static.
public class Service { public static Product[] GetProducts() {return product; } public static Customer[] GetCustomers() {return customers; } public static Order[] GetOrders() { return orders; } public static Product[] product = new Product[]{ new Product{ ProductID = 1, ProductName = "Käse", Price = 10}, new Product{ ProductID = 2, ProductName = "Wurst", Price = 5}, new Product{ ProductID = 3, ProductName = "Obst", Price = 8.56}, new Product{ ProductID = 4, ProductName = "Gemüse", Price = 4}, new Product{ ProductID = 5, ProductName = "Fleisch", Price = 17.5}, new Product{ ProductID = 6, ProductName = "Süßwaren", Price = 3}, new Product{ ProductID = 7, ProductName = "Bier", Price = 2.8}, new Product{ ProductID = 8, ProductName = "Pizza", Price = 7} }; public static Order[] orders = new Order[] { new Order{ OrderID= 1, ProductID = 4, Quantity = 2, Shipped = true}, new Order{ OrderID= 2, ProductID = 1, Quantity = 1, Shipped = true}, new Order{ OrderID= 3, ProductID = 5, Quantity = 4, Shipped = false}, new Order{ OrderID= 4, ProductID = 4, Quantity = 5, Shipped = true}, new Order{ OrderID= 5, ProductID = 8, Quantity = 6, Shipped = true}, new Order{ OrderID= 6, ProductID = 3, Quantity = 3, Shipped = false}, new Order{ OrderID= 7, ProductID = 7, Quantity = 2, Shipped = true}, new Order{ OrderID= 8, ProductID = 8, Quantity = 1, Shipped = false}, new Order{ OrderID= 9, ProductID = 4, Quantity = 1, Shipped = false}, new Order{ OrderID= 10, ProductID = 1, Quantity = 8, Shipped = true}, new Order{ OrderID= 11, ProductID = 3, Quantity = 3, Shipped = true}, new Order{ OrderID= 12, ProductID = 6, Quantity = 6, Shipped = true}, new Order{ OrderID= 13, ProductID = 1, Quantity = 4, Shipped = false}, new Order{ OrderID= 14, ProductID = 6, Quantity = 3, Shipped = true}, new Order{ OrderID= 15, ProductID = 5, Quantity = 7, Shipped = true}, new Order{ OrderID= 16, ProductID = 1, Quantity = 9, Shipped = true} }; public static Customer[] customers = new Customer[]{ new Customer{ Name = "Herbert", City = Cities.Aachen, Orders = new Order[]{orders[3], orders[2], orders[8], orders[10] } }, new Customer{ Name = "Willi", City = Cities.Köln, Orders = new Order[]{orders[6], orders[7], orders[9] } }, new Customer{ Name = "Hans", City = Cities.Bonn, Orders = new Order[]{orders[4], orders[11], orders[14] } }, new Customer{ Name = "Freddy", City = Cities.Bonn, Orders = new Order[]{orders[1], orders[5], orders[13] } }, new Customer{ Name = "Theo", City = Cities.Aachen, Orders = new Order[]{orders[15], orders[12] } } }; }
Sollten Sie selbst in einem eigenen Projekt mit den Daten experimentieren, müssen Sie die Assembly Musterdaten.dll unter Verweise in das Projekt einbinden. Zudem sollten Sie auch den Namespace Musterdaten mit using importieren.
8.3.2 Die Abfrage-Syntax 

LINQ ähnelt SQL. Mit from wird die Datenquelle angegeben, mit where das Filterkriterium, und mit select legen Sie fest, welche Daten zurückgegeben werden. Dazu gesellt sich eine große Zahl weiterer Operatoren, die weitere Operationen auf den abzufragenden Daten ausführen.
Eine LINQ-Abfrage beginnt mit from und nicht wie unter SQL mit select. Der Grund dafür ist, dass zuerst die Datenquelle ausgewählt sein muss, auf der alle nachfolgenden Operationen ausgeführt werden. Dies gestattet es uns darüber hinaus, mit der IntelliSense-Liste im Codeeditor zu arbeiten.
Mit der folgenden LINQ-Abfrage können wir uns die Kundennamen mit einer Länge von weniger als sechs Zeichen sortiert ausgeben lassen:
Customer[] customers = Service.GetCustomers();
var cust = from customer in customers
where customer.Name.Length < 6
select customer.Name;
foreach (var p in cust)
Console.WriteLine(p);Beachten Sie, dass eine LINQ-Abfrage mit einem Semikolon abgeschlossen wird. In der Anweisung
from customer in customers
wird die Liste customers durchlaufen. Die einzelnen Einträge werden danach mit where nach dem vorgegebenen Kriterium selektiert. select gibt an, welche Elemente eines Customer-Objekts in die Ergebnisliste eingetragen werden. In unserem Code ist es nur der Name des Kunden. Das Ergebnis wird einer implizit typisierten Variablen zugewiesen. Diese Anweisung könnte auch durch
IEnumerable<string> cust = from customer in customers
ersetzt werden, da eine LINQ-Abfrage Collections vom Typ IEnumerable<T> und IEnumerable abfragt.
In LINQ werden grundsätzlich zwei Schreibweisen unterschieden:
- Abfrage-Syntax (Query Expression-Syntax)
- Erweiterungsmethoden-Syntax (Extension Method-Syntax)
Ich habe Ihnen die Abfrage-Syntax gezeigt. Wie Sie sehen, kommen dabei die neuen Schlüsselwörter from, where und select von C# zum Einsatz.
Nun möchte ich Ihnen auch die gleiche Abfrage in der Erweiterungsmethoden-Syntax vorstellen. Diese ist zwar schwerer zu lesen, gestattet Ihnen aber, die volle Leistungsfähigkeit von LINQ auszuschöpfen. Unser Beispiel von oben würde in dieser Schreibweise folgendermaßen geschrieben:
var cust = customers
.Where(customer => customer.Name.Length < 6)
.Select(c => c.Name);Sie können sehr schön erkennen, wie bei dieser Schreibweise mit Where und Select tatsächlich Erweiterungsmethoden zum Einsatz kommen, denen Lambda-Ausdrücke übergeben werden.
Nicht alle Abfrageausdrücke lassen sich in der Schreibweise der Abfrage-Syntax ausdrücken. In einigen Fällen kommen Sie an der Erweiterungsmethoden-Syntax nicht vorbei. Sie können beide Schreibweisen mischen. In jedem Fall wandelt der Compiler die Abfrage in die Erweiterungsmethoden-Syntax um.
8.3.3 Übersicht über die Abfrageoperatoren 

LINQ stellt Ihnen zahlreiche Abfrageoperatoren zur Verfügung. Alle sind in der Klasse Enumerable im Namespace System.Linq definiert. Die Abfrageoperatoren sind als Erweiterungsmethoden implementiert, die für Typen gelten, die die Schnittstelle IEnumerable<T> implementieren.
In Tabelle 8.2 sind alle LINQ-Abfrageoperatoren angegeben.
| Operatortyp | Operator |
|
Aggregatoperatoren |
Aggregate, Average, Count, LongCount, Min, Max, Sum |
|
Castingoperatoren |
Cast, OfType, ToArray, ToDictionary, ToList, ToLookup, ToSequence |
|
Elementoperatoren |
DefaultIfEmpty, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault |
|
Gleichheitsoperatoren |
EqualAll |
|
Sequenzoperatoren |
Empty, Range, Repeat |
|
Gruppierungsoperatoren |
GroupBy |
|
Join-Operatoren |
Join, GroupJoin |
|
Sortieroperatoren |
OrderBy, ThenBy, OrderByDescending, ThenByDescending, Reverse |
|
Aufteilungsoperatoren |
Skip, SkipWhile, Take, TakeWhile |
|
Quantifizierungsoperatoren |
All, Any, Contains |
|
Restriktionsoperatoren |
Where |
|
Projektionsoperatoren |
Select, SelectMany |
|
Set-Operatoren |
Concat, Distinct, Except, Intersect, Union |
Wir werden im weiteren Verlauf des Kapitels auf die meisten der hier aufgeführten LINQ-Operatoren genauer eingehen.
8.3.4 Die »from«-Klausel 

Ein Abfrageausdruck beginnt mit der from-Klausel. Diese beschreibt, welche Datenquelle abgefragt werden soll, und definiert eine lokale Bereichsvariable, die jedes Element in der Datenquelle repräsentiert. Die Datenquelle muss entweder die Schnittstelle IEnumerable<T> oder IEnumerable implementieren. Zu den abfragbaren Datenquellen zählen auch diejenigen, die sich auf IQueryable<T> zurückführen lassen.
Datenquelle und Bereichsvariable sind streng typisiert. Wenn Sie mit
from customer in customers
das Array aller Kunden als Datenquelle angeben, ist die Bereichsvariable vom Typ Customer.
Etwas anders ist der Sachverhalt, wenn die Datenquelle beispielsweise vom Typ ArrayList ist. Wie Sie wissen, können in einer ArrayList Objekte unterschiedlichsten Typs verwaltet werden. Um auch solche Datenquellen abfragen zu können, muss die Bereichsvariable explizit typisiert werden, z. B.:
ArrayList arr = new ArrayList();
arr.Add(new Circle());
arr.Add(new Circle());
var cust = from Circle kreis in arr
select kreis;Manchmal kommt es vor, dass jedes Element einer Datenquelle seinerseits selbst eine Liste untergeordneter Elemente beschreibt. Ein gutes Beispiel dafür ist in unserer Anwendung zu finden, die die Musterdaten bereitstellt.
public class Customer {
public string Name;
public Cities City;
public Order[] Orders;
}Jedem Kunden ist ein Array vom Typ Order zugeordnet. Um die Bestellungen abzufragen, muss eine weitere from-Klausel angeführt werden, die auf die Bestellliste des jeweiligen Kunden zugreift. Jede from-Klausel kann separat mit where gefiltert oder mit orderby sortiert werden.
var p = from cust in customers
where cust.Name == "Hans"
from order in cust.Orders
where order.Quantity > 6
select order.OrderID;In diesem Codefragment wird die Liste aller Kunden zuerst nach Hans durchsucht. Die gefundene Dateninformation extrahiert anschließend die Bestellinformationen und beschränkt das Ergebnis auf alle Bestellungen von Hans, die eine Bestellmenge > 6 haben.
8.3.5 Der Restriktionsoperator »where« 

Angenommen, Sie möchten alle Kunden auflisten, deren Wohnort Aachen ist. Um eine Folge von Elementen zu filtern, verwenden Sie den Where-Operator.
Customer[] customers = Service.GetCustomers();
var result = from cust in customers
where cust.City == Cities.Aachen
select cust.Name;
foreach (var p in result)
Console.WriteLine(p);Mit dem Select-Operator geben Sie das Element an, das in die Ergebnisliste aufgenommen werden soll. In diesem Fall ist das der Name jeder entsprechend durch den Where-Operator gefundenen Person. Die Ergebnisliste wird in der foreach-Schleife durchlaufen und an der Konsole ausgegeben. Sie werden Herbert und Theo in der Ergebnisliste finden.
Sie können die Abfrage-Syntax auch durch die Erweiterungsmethoden-Syntax ersetzen. Geben Sie dabei direkt das zu durchlaufende Array an. An der Codierung der Konsolenausgabe ändert sich nichts.
var result = customers
.Where( cust => cust.City == Cities.Aachen)
.Select(cust => cust.Name);Um aus einem Customer-Objekt für die Ergebnisliste mehrere spezifische Daten zu filtern, übergeben Sie dem Select-Operator einen anonymen Typ, der sich aus den gewünschten Elementen zusammensetzt. Interessiert Sie beispielsweise neben dem Namen auch das Alter der gefundenen Person in der Ergebnisliste, sieht der Code der LINQ-Abfrage wie folgt aus:
var result = from cust in customers
where cust.City == Cities.Aachen
select new { cust.Name, cust.City };
foreach (var p in result)
Console.WriteLine("{0,-7}{1}", p.Name, p.City);Die Ergebnisliste setzt sich fortan aus den objektspezifischen Elementen Alter und Name zusammen und muss bei der Ausgabe beachtet werden.
Mehrere Filterkriterien zu berücksichtigen ist nicht weiter schwierig. Sie müssen nur den Where-Operator ergänzen. Dazu benutzen Sie die C#-spezifischen Operatoren. Im nächsten Codefragment werden alle noch nicht ausgelieferten Bestellungen gesucht, deren Bestellmenge größer 3 ist.
Order[] orders = Service.GetOrders();
var result = from order in orders
where order.Quantity > 3 && order.Shipped == false
select order.OrderID;oder:
var result = orders
.Where(order => order.Quantity > 3 &&
order.Shipped == false)
.Select(ord => ord.OrderID);Die Überladungen des Where-Operators
Wenn Sie sich die .NET-Dokumentation des Where-Operators ansehen, finden Sie die beiden folgenden Signaturen:
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, int, bool> predicateDie erste wird für Abfragen verwendet, wie wir sie weiter oben eingesetzt haben. Die IEnumerable<T>-Collection wird dabei komplett gemäß den Filterkriterien durchsucht.
Mit der zweiten Signatur können Sie den Bereich der Ergebnisliste einschränken, und zwar anhand des nullbasierten Index, der als Integer angegeben wird. Nehmen wir an, Sie interessieren sich für alle Bestellungen, deren Bestellmenge > 3 ist. Allerdings möchten Sie, dass die Ergebnisliste sich auf Indizes in der Datenquelle beschränkt, die < 10 sind. Es werden demnach nur die Indizes 0–9 berücksichtigt.
var result = orders
.Where((order, index) => order.Quantity > 3 &&
index < 10)
.Select(ord => ord.OrderID);Hier müssen Sie die Schreibweise der Erweiterungsmethoden-Syntax einsetzen, um der überladenen Erweiterungsmethode Where die erforderlichen Argumente übergeben zu können.
Wie funktioniert der »Where«-Operator?
Betrachten wir noch einmal die folgende Anweisung:
var result = customers
.Where( cust => cust.City == Cities.Aachen)Where ist eine Erweiterungsmethode der Schnittstelle IEnumerable<T> und gilt auch für das Array vom Typ Customer. Der Ausdruck
cust => cust.City == Cities.Aachen
ist ein Lambda-Ausdruck, im eigentlichen Sinne demnach also das Delegate auf eine anonyme Methode. In der Definition des Where-Operators wird dieses Delegate durch das Delegate
Func<T, bool> predicate
beschrieben (siehe Definition von Where oben). Der generische Typparameter T wird durch die Elemente in der zugrunde liegenden Collection beschrieben, die bekanntlich die Schnittstelle IEnumenerable<T> implementiert. In unserem Beispiel handelt es sich um Customer-Objekte. Daher können wir bei korrekter Codierung innerhalb des Lambda-Ausdrucks auch auf die IntelliSense-Liste zurückgreifen. Der zweite Parameter teilt uns mit, von welchem Datentyp der Rückgabewert des Lambda-Ausdrucks ist. Hier wird ein boolescher Typ vorgegeben, denn über true weiß LINQ, dass auf das untersuchte Element das Suchkriterium zutrifft und bei einer Rückgabe von false eben nicht.
Das Zusammenspiel zwischen den neuen Lambda-Ausdrücken und Erweiterungsmethoden im Kontext generischer Typen und Delegates ist hier sehr gut zu erkennen. In ähnlicher Weise funktionieren auch viele andere Operatoren. Ich werde daher im Folgenden nicht jedes Mal erneut das komplexe Zusammenspiel der verschiedenen Sprachkomponenten erörtern.
8.3.6 Die Projektionsoperatoren 

Der »Select«-Operator
Der Select-Operator macht die Ergebnisse der Abfrage über ein Objekt verfügbar, das die Schnittstelle IEnumerable<T> implementiert, z. B.:
var result = from order in orders
select order.OrderID;oder alternativ:
var result = orders.Select(order => order.OrderID);
Die Rückgabe ist in beiden Fällen eine Liste mit den Bestellnummern der in der Collection vertretenen Bestellungen.
Liefert der Select-Operator eine Liste mit neu strukturierten Datenzeilen, müssen Sie einen anonymen Typ als Ergebnismenge definieren:
var result = from customer in customers
select new { customer.Name, customer.City };Der Operator »SelectMany«
SelectMany kommt dann sinnvoll zum Einsatz, wenn es sich bei den einzelnen Elementen in einer Elementliste um Arrays handelt, deren Einzelelemente von Interesse sind. In der Anwendung Musterdaten trifft das auf alle Objekte vom Typ Customer zu, weil die Bestellungen in einem Array verwaltet werden.
var result = from cust in customers
where cust.Name == "Hans"
from order in cust.Orders
where order.Quantity > 6
select order.OrderID;Weiter oben hatten wir uns bereits mit Untermengen dieser Art beschäftigt. Allerdings hatte ich Ihnen noch nicht die Erweiterungsmethoden-Syntax vorgestellt. Das soll nun geschehen, denn dazu dient der Operator SelectMany:
var result = customers
.Where(cust => cust.Name == "Hans")
.SelectMany(cust => cust.Orders)
.Where(order => order.Quantity > 6)
.Select(order => order.OrderID);8.3.7 Sortieroperatoren 

Sortieroperatoren ermöglichen eine Sortierung von Elementen in Ausgabefolgen mit einer angegebenen Sortierrichtung.
Mit dem Operator OrderBy können Sie auf- und absteigend sortieren, mit OrderByDescending nur absteigend. Hier sehen Sie ein Beispiel für eine aufsteigende Sortierung. Dabei werden die Bestellmengen aller Bestellungen der Reihe nach in die Ergebnisliste geschrieben.
Order[] orders = Service.GetOrders();
var result = from order in orders
orderby order.Quantity
select new { order.OrderID, order.Quantity };
foreach (var temp in result)
Console.WriteLine("ID: {0,-3}{1}", temp.OrderID, temp.Quantity);Sehen wir uns diese LINQ-Abfrage noch in der Erweiterungsmethoden-Syntax an:
var result = orders
.OrderBy( order => order.Quantity)
.Select(order => new{order.OrderID, order.Quantity});Durch die Ergänzung von descending lässt sich auch eine absteigende Sortierung erzwingen:
... orderby order.Quantity descending ...
Das folgende Codefragment zeigt, wie Sie mit dem Operator OrderByDescending zum gleichen Ergebnis kommen:
var result = orders
.OrderByDescending( order => order.Quantity)
.Select(order => new{order.OrderID, order.Quantity});Wenn Sie mehrere Sortierkriterien festlegen wollen, helfen Ihnen die beiden Operatoren ThenBy und ThenByDescending weiter. Deren Einsatz setzt aber voraus, dass vorher OrderBy oder OrderByDescending verwendet worden sind. Nehmen wir an, die erste Sortierung soll die Bestellmenge berücksichtigen und die zweite, ob die Bestellung bereits ausgeliefert ist. Die Anweisung dazu lautet:
Order[] orders = Service.GetOrders();
var result = orders
.OrderBy( order => order.Quantity)
.ThenBy ( order => order.Shipped)
.Select( order => new { order.OrderID, order.Quantity,
order.Shipped });
foreach (var temp in result)
Console.WriteLine("ProductID: {0,-3}Menge:{1,-4} Geliefert:{2}",
temp.OrderID, temp.Quantity, temp.Shipped);Manchmal kann es vorkommen, dass Sie die gesamte Ergebnisliste in umgekehrter Reihenfolge benötigen. Hier kommt der Operator Reverse zum Einsatz, der am Ende auf die Ergebnisliste angewendet wird:
var result = orders
.Select(order => new { order.ProductID, order.Quantity })
.Reverse();Wie Sie wissen, werden einige Abfrageoperatoren als Schlüsselwörter von C# angeboten und gestatten die sogenannte Abfrage-Syntax. Reverse und ThenBy zählen nicht dazu. Möchten Sie die von einer Abfrage-Syntax gelieferte Ergebnismenge umkehren, können Sie sich eines kleinen Tricks bedienen. Sie schließen die Abfrage-Syntax in runde Klammern ein und können darauf den Punktoperator mit folgendem Reverse angeben:
var result = (from order in orders
select new { order.ProductID, order.Quantity })
.Reverse();8.3.8 Gruppieren mit »GroupBy« 

Manchmal ist es notwendig, Ergebnisse anhand spezifischer Kriterien zu gruppieren. Dazu dient der Operator GroupBy. Machen wir uns das zuerst an einem Beispiel deutlich. Ausgangspunkt sei wieder das Array mit Customer-Objekten.
// ---------------------------------------------------------// Beispiel: ...\Kapitel 8\GroupBy //----------------------------------------------------------
class Program { static void Main(string[] args) { Customer[] customers = Service.GetCustomers(); var result = customers .GroupBy(cust => cust.City); foreach (IGrouping<Cities, Customer> temp in result) { Console.WriteLine(new string('=', 40)); Console.WriteLine("Stadt: {0}", temp.Key); Console.WriteLine(new string('-', 40)); foreach (var item in temp) Console.WriteLine(" {0}", item.Name); } Console.ReadKey(); } }
Die Ausgabe an der Konsole sehen Sie in Abbildung 8.1.
Abbildung 8.1 Die Ausgabe des Beispielprogramms »GroupBy«
Der Operator GroupBy ist vielfach überladen. Sehen wir uns eine Überladung an:
public static IEnumerable<IGrouping<K,T>> GroupBy<T,K>( this IEnumerable<T> source, Func<T,K> keyselector);
Alle Überladungen geben dabei den Typ IEnumerable<IGrouping<K,T>> zurück. Die Schnittstelle IGrouping<K,T> ist dabei eine spezialisierte Form von IEnumerable<T>.
public interface IGrouping<K,T> : IEnumerable<T> {
K key { get; }
}Betrachten wir nun die äußere Schleife:
foreach (IGrouping<Cities, Customer> temp in result)
Sie müssen der Schnittstelle IGrouping im ersten Typparameter in unserem Beispiel Cities zuweisen, den Datentyp des Elements, nach dem gruppiert werden soll. Der zweite Typparameter beschreibt den Typ des zu gruppierenden Elements.
Die äußere Schleife beschreibt die einzelnen Gruppen und gibt als Resultat alle die Elemente zurück, die zu der entsprechenden Gruppe gehören. In unserem Beispielcode wird diese Untergruppe in der Variablen item erfasst. In der inneren Schleife werden anschließend alle Elemente von temp durchlaufen und die gewünschten Informationen ausgegeben.
Der GroupBy-Operator kann auch in der Schreibweise der Abfrage-Syntax dargestellt werden.
var result = from customer in customers
group customer by customer.City8.3.9 Verknüpfungen mit »Join« 

Mit dem Join-Operator definieren Sie Beziehungen zwischen mehreren Auflistungen, ähnlich wie Sie in SQL mit dem gleichnamigen Join-Statement Tabellen miteinander in Beziehung setzen.
In unseren Musterdaten liegen insgesamt 16 Bestellungen vor. Es soll nun für jede Bestellung die Bestellnummer des bestellten Artikels, die Bestellmenge und der Einzelpreis des Artikels ausgegeben werden. Die Listen der Produkte und Bestellungen spielen in diesem Fall eine entscheidende Rolle.
Order[] orders = Service.GetOrders();
Product[] products = Service.GetProducts();
var liste = orders
.Join(products,
ord => ord.ProductID,
prod => prod.ProductID,
(a, b) => new {a.OrderID,
a.ProductID,
b.Price,
a.Quantity});
foreach(var m in liste)
Console.WriteLine("Order: {0,-3} Product: {1} Menge: {2} Preis: {3}",
m.OrderID, m.ProductID, m.Quantity, m.Price);Der Join-Operator ist überladen. In diesem Beispiel haben wir den folgenden benutzt:
public static IEnumerable<V> Join<T, U, V, K>(
this Enumerable<T> outer,
IEnumerable<U> inner,
Func<T, K> outerKeySelector,
Func<U, K> innerKeySelector,
Func<T, U, V> resultSelector);Join wird als Erweiterungsmethode der Liste definiert, auf die Join aufgerufen wird. In unserem Beispiel ist es die durch orders beschriebene Liste aller Bestellungen. Die innere Liste wird durch das Argument beschrieben und ist in unserem Beispielcode die Liste aller Produkte products. Als zweites Argument erwartet Join im Parameter outerKeySelector das Schlüsselfeld der äußeren Liste (hier: orders), das im vierten Argument mit dem Schlüsselfeld der inneren Liste (hier: products) in Beziehung gesetzt wird.
Im vierten Argument wird die Ergebnisliste bestimmt. Dazu werden zwei Parameter übergeben: Der erste projiziert ein Element der äußeren Liste, der zweite ein Element der inneren Liste in das Ergebnis der Join-Abfrage.
Beachten Sie, dass in der Definition von Join der generische Typ T die äußere Liste beschreibt und der Typ U die innere. Die Schlüssel (in unserem Beispiel werden dazu die Felder genommen), die die ProductID beschreiben, verstecken sich hinter dem generischen Typ K, die Ergebnisliste hinter V.
Sie können eine Join-Abfrage auch in Abfragesyntax notieren:
var liste = from ord in orders
join prod in products
on ord.ProductID equals prod.ProductID
select new { ord.OrderID, ord.ProductID,
prod.Price, ord.Quantity};Die Ergebnisliste sehen Sie in Abbildung 8.2.
Sie sollten darauf achten, dass Sie beim Vergleich links von equals den Schlüssel der äußeren Liste angeben, rechts davon den der inneren. Wenn Sie beide vertauschen, erhalten Sie einen Compilerfehler.
Abbildung 8.2 Resultat der Join-Abfrage
Der Operator »GroupJoin«
Join führt Daten aus der linken und rechten Liste genau dann zusammen, wenn die angegebenen Kriterien alle erfüllt sind. Ist eines oder sind mehrere der Kriterien nicht erfüllt, so entsteht kein Datensatz in der Ergebnismenge. Damit ist der Join-Operator mit dem INNER JOIN-Statement einer SQL-Abfrage vergleichbar.
Wenn Sie ein Äquivalent zu einem LEFT OUTER JOIN oder RIGHT OUTER JOIN suchen, hilft Ihnen der GroupJoin-Operator weiter. Nehmen wir an, Sie möchten wissen, welche Bestellungen für die einzelnen Produkte vorliegen. Sie können die LINQ-Abfrage dann wie folgt definieren:
var liste = products
.GroupJoin(customers.SelectMany(cust => cust.Orders),
prod => prod.ProductID,
ord => ord.ProductID,
(a, b) => new { a.ProductID, Orders = b });
foreach (var t in expr) {
Console.WriteLine("ProductID: {0}", t.ProductID, t.Orders);
foreach (var order in t.Orders)
Console.WriteLine(" OrderID: {0}", order.OrderID);
}GroupJoin arbeitet sehr ähnlich wie der Join-Operator. Der Unterschied zwischen den beiden Operatoren besteht darin, was in die Ergebnismenge aufgenommen wird. Mit Join sind es nur Daten, deren Schlüssel sowohl in der outer-Liste als auch der inner-Liste vertreten sind. Findet Join in der inner-Liste kein passendes Element, wird das outer-Element nicht in die Ergebnisliste aufgenommen.
Ganz anders ist das Verhalten von GroupJoin. Dieser Operator nimmt auch dann ein Element aus der outer-Liste in die Ergebnisliste auf, wenn keine entsprechenden Daten in inner vorhanden sind. Sie können das sehr schön in Abbildung 8.3 sehen, denn der Artikel mit der ProductID = 2 ist in keiner Bestellung zu finden.
Abbildung 8.3 Ergebnisliste der LINQ-Abfrage mit dem »GroupJoin«-Operator
Sie können den GroupJoin-Operator auch in einem Abfrageausdruck beschreiben. Er wird mit join... into... definiert.
Product[] products = Service.GetProducts();
Customer[] customers = Service.GetCustomers();
var liste = from cust in customers
from ord in cust.Orders
select ord;
var expr = from prod in products
join custord in liste
on prod.ProductID equals custord.ProductID into allOrders
select new { prod.ProductID, Orders = allOrders};Alle Beispiele dieses Abschnitts finden Sie auf der Buch-DVD unter \Beispiele\Kapitel 8\Join.
8.3.10 Die Set-Operatoren-Familie 

Der Operator »Distinct«
Vielleicht kennen Sie die Wirkungsweise von DISTINCT bereits von SQL. In LINQ hat der Distinct-Operator die gleiche Aufgabe: Er garantiert, dass in der Ergebnismenge ein Element nicht doppelt auftritt.
string[] cities = new string[]{
"Aachen", "Köln", "Bonn", "Aachen", "Bonn", "Frankfurt"};
var liste = (from p in cities
select p).Distinct();
foreach (string city in liste)
Console.WriteLine(city);Im Array cities kommen die beiden Städte Aachen in Bonn je zweimal vor. Der auf die Ergebnismenge angewendete Distinct-Opertor erkennt dies und sorgt dafür, dass jede Stadt nur einmal angezeigt wird.
Der Operator »Union«
Der Union-Operator verbindet zwei Listen miteinander. Dabei werden doppelte Vorkommen ignoriert.
string[] cities = new string[]{
"Aachen", "Bonn", "Aachen", "Frankfurt"};
string[] namen = new string[]{
"Peter", "Willi", "Hans"};
var listeCities = from c in cities
select c;
var listeNamen = from n in namen
select n;
var listeComplete = listeCities.Union(listeNamen);
foreach (var p in listeComplete)
Console.WriteLine(p);In der Ergebnisliste werden der Reihe nach Aachen, Köln, Bonn, Frankfurt, Peter, Willi und Hans erscheinen.
Der Operator »Intersect«
Der Intersect-Operator bildet eine Ergebnisliste aus zwei anderen Listen. In der Ergebnisliste sind aber nur die Elemente enthalten, die in beiden Listen gleichermaßen enthalten sind. Intersect bildet demnach eine Schnittmenge ab.
string[] cities1 = new string[]{
"Aachen", "Köln", "Bonn", "Aachen", "Frankfurt"};
string[] cities2 = new string[]{
"Düsseldorf", "Bonn", "Bremen", "Köln"};
var listeCities1 = from c in cities1
select c;
var listeCities2 = from n in cities2
select n;
var listeComplete = listeCities1.Intersect(listeCities2);
foreach (var p in listeComplete)
Console.WriteLine(p);Das Ergebnis wird durch die Städte Köln und Bonn gebildet.
Der Operator »Except«
Während Intersect die Gemeinsamkeiten aufspürt, sucht der Operator Except nach allen Elementen, durch die sich die Listen voneinander unterscheiden. Dabei sind nur die Elemente in der Ergebnisliste enthalten, die in der ersten Liste angegeben sind und in der zweiten Liste fehlen.
WennSie in dem Codefragment anstelle von Intersect den Operator Except verwenden, enthält die Ergebnisliste die Orte Aachen und Frankfurt.
8.3.11 Die Familie der Aggregatoperatoren 

LINQ stellt mit Count, LongCount, Sum, Min, Max, Average und Aggregate eine Reihe von Aggregatoperatoren zur Verfügung, um Berechnungen an Quelldaten durchzuführen.
Die Operatoren »Count« und »LongCount«
Sehr einfach einzusetzen sind die beiden Operatoren Count und LongCount. Beide unterscheiden sich dahingehend, dass Count einen int als Typ zurückgibt und LongCount einen long.
Um Count zu testen, wollen wir zuerst wissen, wie viele Bestellungen insgesamt eingegangen sind:
Order[] orders = Service.GetOrders();
var anzahl = (from x in orders
select x).Count();
Console.WriteLine("Anzahl der Bestellungen gesamt = {0}", anzahl);Alternativ können Sie auch Folgendes formulieren:
var anzahl = orders.Count();
Das Ergebnis lautet 16.
Vielleicht interessiert uns auch, wie viele Bestellungen jeder einzelne Kunde aufgegeben hat. Wir müssen dann den folgenden Code schreiben:
Customer[] customers = Service.GetCustomers();
var orderCounts = from c in customers
select new { c.Name, OrderCount = c.Orders.Count() };
foreach (var k in orderCounts)
Console.WriteLine("{0} - {1}", k.Name, k.OrderCount);Der Operator »Sum«
Sum ist grundsätzlich zunächst einmal sehr einfach einzusetzen. Der Operator liefert eine Summe als Ergebnis der LINQ-Abfrage. Im folgenden Codefragment wird die Summe aller Integerwerte ermittelt, die das Array bilden. Das Ergebnis lautet 114.
int[] arr = new int[] { 1, 3, 7, 4, 99 };
var sumInt = arr.Sum();
Console.WriteLine("Integer-Summe = {0}", sumInt);Das folgende Beispiel ist nicht mehr so einfach. Hier soll der Gesamtbestellwert über alle Produkte für jeden Kunden ermittelt werden.
var allOrders =
from cust in customers
from ord in cust.Orders
join prod in products on ord.ProductID equals prod.ProductID
select new { cust.Name, ord.ProductID,
OrderAmount = ord.Quantity * prod.Price };
var summe =
from cust in customers
join ord in allOrders
on cust.Name equals ord.Name into custWithOrd
select new { cust.Name, TotalSumme = custWithOrd.Sum(s => s.OrderAmount) };
foreach(var s in summe)
Console.WriteLine("Name: {0,-7} Bestellsumme: {1}",
s.Name, s.TotalSumme);Analysieren wir den Code schrittweise, und überlegen wir, was das Resultat des folgenden Abfrageteilausdrucks ist.
var allOrders =
from cust in customers
from ord in cust.Orders
join prod in products on ord.ProductID equals prod.ProductID
select new { cust.Name, ord.ProductID,
OrderAmount = ord.Quantity * prod.Price };Zuerst ist es notwendig, die Bestellungen aus jedem Customer-Objekt zu filtern. Danach wird ein Join gebildet, der die ProductIDs aus den einzelnen Bestellungen eines Kunden mit der ProductID aus der Liste der Artikel verbindet. Das Ergebnis ist eine Art Tabelle mit Spalten für den Bestellernamen, die ProductID und die Gesamtsumme für diesen Artikel, die anhand der Bestellmenge gebildet wurde (siehe Abbildung 8.4).
Abbildung 8.4 Bestellwert als Zwischenergebnis
Nun gilt es noch, die Ergebnisliste nach den Kunden zu gruppieren und dann die Gesamtsumme aller Bestellungen zu bilden.
var summe =
from cust in customers
join ord in allOrders
on cust.Name equals ord.Name into custWithOrd
select new { cust.Name,
TotalSumme = custWithOrd.Sum(s => s.OrderAmount) };Wir sollten uns daran erinnern, dass der GroupJoin-Operator mit diesen Fähigkeiten ausgestattet ist. Es müssen zuerst die beiden Listen customers und allOrders zusammengeführt werden. Sie können sich das so vorstellen, dass die Gruppierung mit GroupJoin zur Folge hat, dass für jeden Customer eine eigene »Tabelle« erzeugt wird, in der alle seine Bestellungen beschrieben sind. Die Variable s steht hier für ein Gruppenelement, letztendlich also für eine Bestellung. Die Gruppierung nach Customer-Objekten gestattet es uns nun, mit dem Operator Sum den Inhalt der Spalte OrderAmount zu summieren.
Das Resultat der kompletten LINQ-Abfrage sehen Sie in Abbildung 8.5.
Abbildung 8.5 Ergebnis der Abfrage der Gesamtbestellsumme
Die Operatoren »Min«, »Max« und »Average«
Die Aggregatoperatoren Min und Max ermitteln den minimalen bzw. maximalen Wert in einer Datenliste, Average das arithmetische Mittel.
Grundsätzlich ist der Einsatz der Operatoren sehr einfach, wie das folgende Codefragment exemplarisch an Max zeigt:
var max = (from p in products
select p.Price).Max();Das funktioniert aber auch nur, solange numerische Werte als Datenquelle vorliegen. Sie brauchen den Code nur wie folgt leicht zu ändern, um festzustellen, dass nun eine ArgumentException ausgelöst wird.
var max = (from p in products
select new { p.Price }).Max();Die Meldung zu der Exception besagt, dass mindestens ein Typ die IComparable-Schnittstelle implementieren muss. In der ersten funktionsfähigen Version des Codes stand in der Ergebnisliste ein numerischer Wert, der der Forderung entspricht. Im zweiten, fehlerverursachenden Codefragment hingegen wird ein anonymer Typ beschrieben, der mit der geforderten Schnittstelle überhaupt nicht dienen kann.
Diese Lösung der Problematik ist nicht schwer. Die Operatoren sind alle so überladen, dass ihnen einen Wertselektor übergeben werden kann. Mit anderen Worten: Geben Sie das gewünschte Element aus der Liste der Elemente, die den anonymen Typ bilden, als zu bewertenden Ausdruck an.
var max = (from p in products
select new { p.Price })
.Max( x => x.Price);8.3.12 Generierungsoperatoren 

Der Operator »Range«
Dieser Operator liefert ausgehend von einem Startwert eine Gruppe von Integerwerten, die aus einem spezifizierten Wertebereich ausgewählt werden. Die Definition des Operators lautet wie folgt:
public static IEnumerable<int> Range(int start, int count);
Bei genauer Betrachtung ist dieser Operator mit einer for-Schleife vergleichbar. Sie übergeben dem ersten Parameter den Startwert und teilen mit dem zweiten Parameter mit, wie oft eine bestimmte Operation ausgeführt werden soll.
Im folgenden Codefragment werden alle Produkte gesucht, deren Preis größer 5 ist. Das ist der definierte Startwert. Unsere Liste wird dabei nur einmal durchlaufen.
Product[] products = Service.GetProducts();
var numbers = Enumerable.Range(5, 1)
.SelectMany(x => (from prod in products
where prod.Price > x
select new { prod.ProductName }));
foreach (var res in numbers) {
Console.WriteLine(res);
}Keine Frage, wir könnten mit einer einfachen where-Bedingung zum gleichen Resultat kommen. Der Range-Operator ist auch viel besser dazu geeignet, mathematische Operationen zu codieren. Dies demonstriert der folgende Code:
var nums = Enumerable.Range(1, 10).Select(x => 2 * x); foreach (var num in nums) Console.WriteLine(num);
Der Operator »Repeat«
Der Repeat-Operator arbeitet ähnlich wie der zuvor besprochene Range-Operator. Repeat gibt eine Gruppe zurück, in der dasselbe Element mehrfach enthalten ist. Die Anzahl der Wiederholungen ist dabei festgelegt.
Auch zu diesem Operator wollen wir uns zunächst die Definition ansehen.
public static IEnumerable<T> Repeat<T>(T element, int count);
Dem ersten Parameter übergeben Sie das Element, das wiederholt werden soll. Dem zweiten Parameter teilen Sie die Anzahl der Wiederholungen mit. Mit
Product[] products = Service.GetProducts();
var prods = Enumerable.Repeat((from p in products
select p.ProductName), 3)
.SelectMany(x => x);
foreach (var p in prods)
Console.WriteLine(p);werden beispielsweise alle Produktnamen dreimal ausgegeben.
8.3.13 Quantifizierungsoperatoren 

Wenn Sie beabsichtigen, die Existenz von Elementen in einer Liste anhand von Bedingungen oder definierten Regeln zu überprüfen, helfen die Quantifizierungsoperatoren Ihnen weiter.
Der Operator »Any«
Any ist ein Operator, der ein Prädikat auswertet und einen booleschen Wert zurückliefert. Nehmen wir an, Sie möchten wissen, ob der Kunde Willi auch das Produkt mit der ID = 6 bestellt hat. Any hilft Ihnen dabei, das festzustellen.
bool result = (from cust in customers
from ord in cust.Orders
where cust.Name == "Willi"
select new { ord.ProductID })
.Any(ord => ord.ProductID == 7);
if (result)
Console.WriteLine("ProductID 3 ist enthalten");
else
Console.WriteLine("ProductID 3 ist nicht enthalten");Die Elemente werden so lange ausgewertet, bis der Operator auf ein Element stößt, das die Bedingung erfüllt.
Der Operator »All«
Während Any schon true liefert, wenn für ein Element die Bedingung erfüllt ist, liefert der Operator All nur dann true, wenn alle untersuchten Elemente der Bedingung entsprechen.
Möchten Sie beispielsweise feststellen, ob alle Preise der Einzelprodukte > 3 sind, genügt die folgende LINQ-Abfrage:
bool result = (from prod in products
select prod).All(p => p.Price > 3);8.3.14 Aufteilungsoperatoren 

Mit where und select filtern Sie eine Datenquelle noch vorgegebenen Kriterien. Das Ergebnis ist anschließend eine neue Menge von Daten, die den Kriterien entspricht. Möchten Sie nur eine Teilmenge der Datenquelle betrachten, ohne Filterkriterien einzusetzen, eignen sich die Aufteilungsoperatoren.
Der Operator »Take«
Sie könnten zum Beispiel daran interessiert sein, nur die ersten drei Produkte aus der Liste aller Produkte auszugeben. Mit dem Take-Operator ist das sehr einfach zu realisieren:
Product[] prods = Service.GetProducts(); var result = prods.Take(3); foreach (var prod in result) Console.WriteLine(prod.ProductName);
Wir greifen in unserem Beispiel auf eine Datenquelle zu, die uns der Aufruf der Methode GetProducts liefert. Natürlich kann die zu untersuchende Datenquelle zuvor durch einen anderen LINQ-Ausdruck gebildet werden:
Product[] prods = Service.GetProducts();
var result = (from prod in prods
where prod.Price > 3
select new { prod.ProductName, prod.Price })
.Take(3);
foreach (var prod in result)
Console.WriteLine("{0,-7}{1}", prod.ProductName, prod.Price);Der Operator »TakeWhile«
Der Operator Take basiert auf einem Integer als Zähler. Sehr ähnlich arbeitet auch TakeWhile. Der Unterschied zum zuvor behandelten Operator ist, dass Sie ein Prädikat angeben können, das als Kriterium der Filterung angesehen wird. TakeWhile durchläuft die Datenquelle und gibt das gefundene Element zurück, wenn das Ergebnis der Prüfung true ist. Beendet wird der Durchlauf unter zwei Umständen:
- Das Ende der Datenquelle ist erreicht.
- Das Ergebnis einer Untersuchung lautet false.
Wir wollen uns das an einem Beispiel ansehen. Auch dabei wird als Quelle auf die Liste der Produkte zurückgegriffen. Das Prädikat sagt aus, dass die Produkte in der Ergebnisliste erfasst werden sollen, deren Preis höher als 3 ist:
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.TakeWhile(n => n.Price > 3);
foreach (var prod in result)
Console.WriteLine("{0,-7}{1}", prod.ProductName, prod.Price);Es werden die folgenden Produkte angezeigt:
- Käse
- Wurst
- Obst
- Gemüse
- Fleisch
Beachten Sie, dass in der Ergebnisliste das Produkt Pizza nicht enthalten ist, da die Schleife beendet wird, ehe Pizza einer Untersuchung unterzogen werden kann.
Die Operatoren »Skip« und »SkipWhile«
Take und TakeWhile werden um Skip und SkipWhile ergänzt.
Skip überspringt eine bestimmte Anzahl von Elementen in einer Datenquelle. Der verbleibende Rest bildet die resultierende Ergebnismenge. Um zum Beispiel die ersten beiden in der Liste enthaltenen Produkte aus der Ergebnisliste auszuschließen, codieren Sie die folgenden Anweisungen:
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.Skip(2);SkipWhile erwartet ein Prädikat. Die Elemente werden damit verglichen. Dabei werden die Elemente so lange übersprungen, wie das Ergebnis der Überprüfung true liefert. Sobald eine Überprüfung false ist, werden das betreffende Element und alle Nachfolgeelemente in die Ergebnisliste aufgenommen.
Das Prädikat im folgenden Codefragment sucht in der Liste aller Produkte nach dem ersten Produkt, für das die Bedingung nicht gilt, dass der Preis > 3 ist. Dieses und alle darauf folgenden Elemente werden in die Ergebnisliste geschrieben.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.SkipWhile(x => x.Price > 3 );Ausgegeben werden folgende Produkte:
- Süßwaren
- Bier
- Pizza
8.3.15 Die Elementoperatoren 

Bisher lieferten uns alle Operatoren immer eine Ergebnismenge zurück. Oft möchten Sie aber aus einer Liste ein bestimmtes einzelnes Element herausfinden. Hierbei unterstützen uns die Operatoren, denen wir uns nun widmen.
Der Operator »First«
Dieser Operator sucht das erste Element in einer Datenquelle. Wegen der Überladung kann es sich um das positional erste Element handeln oder um das erste Element einer mit einem Prädikat gebildeten Ergebnisliste.
Das folgende Beispiel zeigt, wie einfach der Einsatz von First ist. Als Ergebnis wird Käse an der Konsole ausgegeben.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName })
.First();
Console.WriteLine("{0}", result.ProductName);Vielleicht möchten Sie aber eine Liste aller Produkte haben, deren Preis kleiner 10 ist und aus dieser Liste nur das erste Listenelement herausfiltern.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.First(item => item.Price < 10);
Console.WriteLine("{0}", result.ProductName);Hier lautet das Produkt Wurst.
Der Operator »FirstOrDefault«
Versuchen Sie einmal, das letzte Codefragment mit dem Prädikat
item => item.Price < 1
auszuführen. Sie werden eine Fehlermeldung erhalten, weil kein Produkt in der Datenquelle enthalten ist, das der genannten Bedingung entspricht.
In solchen Fällen empfiehlt es sich, anstelle des Operators First den Operator FirstOrDefault zu benutzen. Für den Fall, dass kein Element gefunden wird, liefert der Operator default(T) zurück. Handelt es sich um einen Referenztyp, ist das null.
FirstOrDefault liegt ebenfalls in zwei Überladungen vor. Sie können neben der parameterlosen Variante auch die parametrisierte Überladung benutzen, der Sie das gewünschte Prädikat übergeben.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.FirstOrDefault(item => item.Price < 1);
if (result == null)
Console.WriteLine("Kein Element entspricht der Bedingung.");
else
Console.WriteLine("{0}", result.ProductName);Die Operatoren »Last« und »LastOrDefault«
Sicherlich können Sie sich denken, dass die beiden Operatoren Last und LastOrDefault Ergänzungen der beiden im Abschnitt zuvor behandelten Operatoren sind. Beide operieren auf die gleich Weise wie First und FirstOrDefault, nur dass das letzte Element der Liste das Ergebnis bildet.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductName, prod.Price })
.LastOrDefault(item => item.Price < 5);
if (result == null)
Console.WriteLine("Kein Element entspricht der Bedingung.");
else
Console.WriteLine("{0}", result.ProductName);Die Operatoren »Single« und »SingleOrDefault«
Alle bislang vorgestellten Elementoperatoren lieferten eine Ergebnismenge, aus der ein Element herausgelöst wurde: Entweder liefern sie das erste oder das letzte Element. Mit Single bzw. SingleOrDefault können Sie nach einem bestimmten, eindeutigen Element Ausschau halten. Eindeutig bedeutet in diesem Zusammenhang, dass es kein Zwischenergebnis gibt, aus dem anschließend ein Element das Ergebnis bildet. In der Musterdaten-Anwendung ist beispielsweise das Feld ProductID eindeutig, vergleichbar mit der Primärschlüsselspalte einer Datenbanktabelle.
Mit Single und SingleOrDefault können Sie ein eindeutiges Element finden. Werden mehrere gefunden, wird eine InvalidOperationException ausgelöst. Auch für dieses Pärchen gilt: Besteht die Möglichkeit, dass kein Element gefunden wird, sollten Sie den Operator SingleOrDefault einsetzen, der – wie bei den anderen Operatoren auch – default<T> als Rückgabewert liefert und keine Ausnahme auslöst wie Single in diesem Fall.
Sie können beide Operatoren parameterlos aufrufen oder ein Prädikat angeben.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductID, prod.ProductName })
.Single( p => p.ProductID == 2);
if (result == null)
Console.WriteLine("Kein Element entspricht der Bedingung.");
else
Console.WriteLine("{0}", result.ProductName);Die Operatoren »ElementAt« und »ElementOrDefault«
Wenn Sie ein bestimmtes Element anhand seiner Position aus einer Liste extrahieren möchten, sollten Sie entweder die Methode ElementAt oder die Methode ElementAtOrDefault verwenden.
Product[] prods = Service.GetProducts();
var result = (from prod in prods
select new { prod.ProductID, prod.ProductName )
.ElementAtOrDefault(3);
if (result == null)
Console.WriteLine("Kein Element entspricht der Bedingung.");
else
Console.WriteLine("{0}", result.ProductName);Beide Methoden erwarten die Angabe des Index in der Liste. Da Listen nullbasiert sind, wird bei der Angabe »3« das vierte Element extrahiert. ElementAtOrDefault liefert wieder den Standardwert, falls der Index negativ oder größer als die Elementanzahl ist.
Der Operator »DefaultIfEmpty«
Standardmäßig liefert dieser Operator eine Liste von Elementen ab. Sollte die Liste jedoch leer sein, führt dieser Operator nicht sofort zu einer Exception. Stattdessen ist der Rückgabewert dann entweder default<T> oder – falls Sie die überladene Fassung von DefaultIfEmpty eingesetzt haben – ein spezifischer Wert.
List<string> liste = new List<string>();
liste.Add("Peter");
liste.Add("Uwe");
foreach (string tempStr in liste.DefaultIfEmpty("leer")) {
Console.WriteLine(tempStr);
}In diesem Codefragment wird vorgegeben, dass bei einer leeren Liste die Zeichenfolge leer das Ergebnis der Operation darstellt.









Jetzt bestellen





