Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einführung
2 Grundlagen der Sprachsyntax
3 Klassendesign
4 Weitere Datentypen
5 Multithreading
6 Collections und LINQ
7 Eingabe und Ausgabe
8 Anwendungen: Struktur und Installation
9 Code erstellen und debuggen
10 Einige Basisklassen
11 Windows-Anwendungen erstellen
12 Die wichtigsten Steuerelemente
13 Tastatur- und Mausereignisse
14 MDI-Anwendungen
15 Grafiken mit GDI+
16 Drucken
17 Entwickeln von Steuerelementen
18 Programmiertechniken
19 WPF – Grundlagen
20 Layoutcontainer
21 WPF-Steuerelemente
22 Konzepte von WPF
23 Datenbankverbindung mit ADO.NET
24 Datenbankabfragen mit ADO.NET
25 DataAdapter
26 Offline mit DataSet
27 Datenbanken aktualisieren
28 Stark typisierte DataSets
A Anhang: Einige Übersichten
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual Basic 2008 von Andreas Kuehnel, Stephan Leibbrandt
Das umfassende Handbuch
Buch: Visual Basic 2008

Visual Basic 2008
3., aktualisierte und erweiterte Auflage, geb., mit DVD
1.323 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1171-0
Pfeil 6 Collections und LINQ
Pfeil 6.1 Sammlungsschnittstellen
Pfeil 6.1.1 IEnumerable
Pfeil 6.1.2 ICollection
Pfeil 6.1.3 IDictionary
Pfeil 6.1.4 IList
Pfeil 6.2 Sammlungsklassen
Pfeil 6.2.1 Collection
Pfeil 6.2.2 List
Pfeil 6.2.3 Wörterbücher
Pfeil 6.2.4 Schlangen
Pfeil 6.2.5 Gleichheit
Pfeil 6.3 Array
Pfeil 6.3.1 Die Eigenschaften eines Array-Objekts
Pfeil 6.3.2 Methoden von Array
Pfeil 6.4 LINQ
Pfeil 6.4.1 Neue Sprachkonzepte
Pfeil 6.4.2 Erweiterungsmethoden
Pfeil 6.4.3 Abfragesyntax
Pfeil 6.4.4 Abfrageoperatoren
Pfeil 6.4.5 From-Klausel
Pfeil 6.4.6 Der Restriktionsoperator Where
Pfeil 6.4.7 Projektionsoperatoren
Pfeil 6.4.8 Sortieroperatoren
Pfeil 6.4.9 Gruppieren mit GroupBy
Pfeil 6.4.10 Verknüpfungen mit Join
Pfeil 6.4.11 Die Set-Operator-Familie
Pfeil 6.4.12 Die Familie der Aggregatoperatoren
Pfeil 6.4.13 Generierungsoperatoren
Pfeil 6.4.14 Quantifizierungsoperatoren
Pfeil 6.4.15 Aufteilungsoperatoren
Pfeil 6.4.16 Die Elementoperatoren


Rheinwerk Computing - Zum Seitenanfang

6.4 LINQ Zur nächsten ÜberschriftZur vorigen Überschrift

Die Analyse von Daten zu programmieren kann recht aufwendig werden. Da die Aufgaben bei sehr verschiedenen Datenquellen immer wieder dieselben sind, wurde die Schnittstelle LINQ (Language Integrated Query) geschaffen. Dadurch können Sie Analysen von Daten programmieren und später mit minimalem Aufwand die Datenquelle ändern. Zum Beispiel können Sie ein Programm mit einfachen Auflistungen entwickeln und erst später die Daten in einer Datenbank bearbeiten – oft ohne eine Zeile Code zu ändern. Für LINQ sind bereits Anbindungen für einige Datenquellen definiert:

  • LINQ to Objects steht im Namensraum System.Linq und ist das Fundament aller LINQ-Abfragen. Datenquellen sind Auflistungen und Objekte, die untereinander in Beziehung gesetzt werden können.
  • LINQ to XML bietet eine Programmierschnittstelle für XML im Arbeitsspeicher, die das in .NET sprachintegrierte Abfrage-Framework nutzt.
  • LINQ to SQL ist Microsofts Provider für das eigene Datenbanksystem SQL Server.
  • LINQ to ADO.NET arbeitet mit einem DataSet oder SQL-Abfragen.

Die Darstellungen in diesem Kapitel basieren auf LINQ to Objects. Da LINQ eine Schnittstellentechnologie ist, ist das keine große Beschränkung. Aufgrund der Nähe zu Datenbankabfragen wird ein LINQ-Aufruf als Abfrageausdruck bezeichnet. Die Syntax ist recht ähnlich zu SQL.


Rheinwerk Computing - Zum Seitenanfang

6.4.1 Neue Sprachkonzepte Zur nächsten ÜberschriftZur vorigen Überschrift

Um die Syntax einfach zu halten, wurden neue Konzepte in Visual Basic eingeführt:

  • Implizit typisierte Variablen: Aus dem Wert wird der Datentyp ermittelt, nicht zu verwechseln mit Option Strict Off. Eingeführt, weil jede Datenabfrage eine unterschiedliche Struktur mit unterschiedlichen Typen hat.
  • Lambda-Ausdrücke: Funktionen außerhalb eines Datentyps (siehe Abschnitt 3.9.5, »Funktionsobjekte: Function(λ-Ausdrücke)«). Eingeführt, um einfach auf die Mitglieder strukturierter Datentypen wie Datenzeilen zugreifen zu können.
  • Anonyme Klassen: Typdefinition ohne eigentliche Klassendefinition (siehe Abschnitt 4.6, »Anonyme Klassen«). Eingeführt, um die Resultate von Abfragen zu speichern.
  • Erweiterungsmethoden: Erweiterungsmethoden dienen zum Nachrüsten von Methoden für beliebige Klassen (siehe Abschnitt 4.8.4, »Klassenerweiterungen: Extension«). Eingeführt, um leichter auf verschiedenen Datenquellen arbeiten zu können.

Sie werden diese Konzepte meistens einsetzen, ohne es überhaupt zu bemerken.

Implizit typisierte Variablen

Die Ableitung eines Datentyps aus einem Wert findet vor der Kompilierung statt und ist auf die Initialisierung lokaler Variablen beschränkt. Lokal sind alle Variablen, die nicht auf Klassenebene definiert sind, also auch Schleifenvariablen. Alle Datentypen im Wert müssen eindeutig sein, das heißt, eine Zuweisung von Nothing ist nicht erlaubt. Die Deklaration von Variablen wird bis auf den fehlenden Datentyp durch die Typinferenz nicht geändert. In den folgenden Zeilen ist der ermittelte Datentyp im Kommentar angegeben.

Dim x = 2.9                  'Double 
Dim a = New Integer() {}     'Integer 
Dim v = From r In a Select r 'IEnumerable(Of Integer) 
For i = 1 To 5 : Next        'Integer

Durch die Ermittlung des Datentyps kann oft auf Delegates zur Spezifikation des Rückgabetyps verzichtet werden.

Im Rahmen von LINQ ist die Ableitung der Datentypen unverzichtbar. An anderer Stelle empfehle ich dringend, darauf zu verzichten. Durch explizite Typangaben wird der Quelltext erheblich leichter lesbar.

Neue Schlüsselwörter

Um die Formulierung von Abfragen einfach und ähnlich zu SQL zu halten, ist Visual Basic um einige speziell interpretierte Bezeichner erweitert worden (siehe Tabelle 6.10). Ich vermeide das Wort Schlüsselwörter, da Bezeichner als normale Variablen verwendet werden können und Schlüsselwörter nicht. Die Bezeichner haben also nur im Rahmen einer LINQ-Syntax eine besondere Bedeutung.


Tabelle 6.10 LINQ-spezifische Bezeichner

Aggregate

Ascending

By

Descending

Distinct

From

Group

Into

Join

Let

Order

Select

Skip

Take

Where



Rheinwerk Computing - Zum Seitenanfang

6.4.2 Erweiterungsmethoden Zur nächsten ÜberschriftZur vorigen Überschrift

LINQ-Abfragen müssen letztendlich in die Programmiersprache Visual Basic übersetzt werden. Zum besseren Verständnis der Abfragesyntax lohnt es sich daher, einmal von der anderen Seite zu kommen und eine Abfrage in normaler Visual-Basic-Syntax zu formulieren und dann zu sehen, wie dieselbe Abfrage in LINQ formuliert wird.

Zentral für die Abfrage sind die zu verwendenden Daten. Da in .NET nichts außerhalb von Objekten (und Klassen) existieren kann, werden Manipulationen der Daten als Operationen auf den Daten formuliert. Die Daten sind dabei in einem Objekt gespeichert, das mehrere Daten gleichen Typs aufnehmen kann. Erlaubt sind:

  • Datentyp IQueryable
  • Datentyp IEnumerable
  • Das Objekt hat eine Methode AsQueryable() As IQueryable.
  • Das Objekt hat eine Methode AsEnumerable() As IEnumerable.
  • Es gibt eine Umwandlungsmethode Cast(Of T)() As IQueryable oder IEnumerable.

Mit diesen grundlegenden Schnittstellen ist fast jede Auflistung erlaubt, und andere Datentypen werden leicht zu kompatiblen Typen. Die Funktionalität steckt in den Erweiterungsklassen Queryable und Enumerable. Tabelle 6.11 listet alle Methoden dieser Klassen auf.


Tabelle 6.11 Methoden von »Enumerable« und »Queryable« (E = nur »Enumerable«)

Methode Beschreibung
Aggregate

Akkumuliert Daten.

All

Gibt an, ob alle Elemente eine Bedingung erfüllen.

Any

Gibt an, ob ein Element eine Bedingung erfüllt.

AsEnumerable 
AsQueryable

Durchreichen der Eingabe

Average

Mittelwert numerischer Elemente

Cast

Konvertierung in IEnumerable(Of T)

Concat

Zwei Auflistungen verbinden

Contains

Gibt an, ob ein Element vorhanden ist.

Count

Anzahl der Elemente

DefaultIfEmpty

Standardwert statt fehlender Werte

Distinct

Doppelte Werte entfernen

ElementAt

Element an der gegebenen Position

ElementAtOrDefault

Element an der gegebenen Position oder Standardwert, wenn das Element fehlt

Except

Komplementärmenge

First

Erstes Element

FirstOrDefault

Erstes Element oder Standardwert, wenn das Element fehlt

GroupBy

Gruppierung nach Schlüsseln

GroupJoin

Kombination jedes Elements der ersten Liste mit je einer Teilmenge der zweiten

Intersect

Schnittmenge

Join

Von der Kombination jedes Wertes mit jedem Wert der zweiten Liste die auswählen, die eine Bedingung erfüllen

Last

Letztes Element

LastOrDefault

Letztes Element oder Standardwert, wenn das Element fehlt

LongCount

Anzahl der Elemente

Max

Maximum der Elemente

Min

Minimum der Elemente

OfType

Nach Typ selektieren

OrderBy

In aufsteigender Reihenfolge sortieren

OrderByDescending

In absteigender Reihenfolge sortieren

Range

Sequenz natürlicher Zahlen

E

Repeat

Menge mit identischen Elementen

Reverse

Umgekehrte Reihenfolge

Select

Transformation jedes Elements

SelectMany

Verbindung der Transformation jedes Elements in eine Sequenz von Werten

SequenceEqual

Gibt an, ob zwei Listen gleich sind.

Single

Das einzige Element, das eine Bedingung erfüllt

SingleOrDefault

Das einzige Element, das eine Bedingung erfüllt oder Standardwert, wenn das Element fehlt

Skip

Restmenge nach gegebener Position

SkipWhile

Restmenge, nachdem eine Bedingung nicht erfüllt ist

Sum

Summe numerischer Werte

Take

Restmenge bis zur gegebenen Position

TakeWhile

Restmenge, solange eine Bedingung erfüllt ist

ThenBy

Nachgeschaltete sekundäre aufsteigende Sortierung

ThenByDescending

Nachgeschaltete sekundäre abssteigende Sortierung

ToArray

In Array konvertieren

E

ToDictionary

In Dictionary(Of TKey, TValue) konvertieren

E

ToList

In List(Of T) konvertieren

E

ToLookup

In Lookup(Of TKey, TElement) konvertieren

E

Union

Vereinigungsmenge

Where

Nach Kriterium selektieren


Damit können wir eine erste Abfrage in Visual-Basic-Syntax formulieren. Das folgende Beispiel definiert eine Klasse Person mit einem Feld für den Namen und einem Feld für das Alter. In der Methode Test() wird eine Liste von Personen erstellt. Da Arrays die Schnittstelle IEnumerable implementieren, können sie in Abfragen benutzt werden. Die eigentliche Abfrage startet mit daten, sortiert diese mit OrderBy und wendet mit ThenBy ein zweites Sortierkriterium an.


'...\Sammlungen\Linq\VBAbfrage.vb

Option Strict On 
Namespace Sammlungen 
  Module VBAbfrage

    Class Person 
      Friend Name As String, Alter As Integer 
      Sub New(ByVal n As String, ByVal a As Integer) 
        Name = n : Alter = a 
      End Sub 
    End Class

    Sub Test() 
      Dim daten() As Person = New Person() {New Person("Emil", 12), _ 
      New Person("Jens", 9), New Person("Marie", 12), New Person("Hugo", 9)}

      Dim v As IEnumerable(Of Person) = daten _ 
                                        .OrderBy(Function(a) a.Alter) _ 
                                        .ThenBy(Function(a) a.Name)

      For Each p As Person In v 
        Console.Write(p.Name & "(" & p.Alter & ") ") 
      Next 
      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Die Sortierung erfolgt nach beiden Kriterien.

Hugo(9) Jens(9) Emil(12) Marie(12)

Sie sehen, wie einfach Sie Abfragen hintereinanderschalten können. Dies ist der Schlüssel zu komplexen Abfragen. Jedes Einzelteil einer Abfrage mündet letztendlich in einen einfachen Methodenaufruf der Klasse Enumerable, die über den in Abschnitt 4.8.4, »Klassenerweiterungen: Extension«, beschriebenen Mechanismus automatisch das Objekt, hier daten und das Resultat von OrderBy, als ersten Parameter bekommt. Es gibt zwar viele Methoden in Enumerable, aber jede für sich ist gut überschaubar. Im Folgenden werden wir diese in LINQ-Syntax näher untersuchen.


Rheinwerk Computing - Zum Seitenanfang

6.4.3 Abfragesyntax Zur nächsten ÜberschriftZur vorigen Überschrift

Die eben formulierte Abfrage können Sie auch in der Abfragesyntax von LINQ formulieren, die sich sehr stark an SQL orientiert. Einer der wenigen Unterschiede ist die Platzierung der From-Klausel. SQL beginnt mit Select, LINQ dagegen mit From. Der Grund ist die konsequente Erzeugung der Daten aus einer Quelle, die daher an erster Stelle genannt wird.

Die Syntaxen in LINQ und in Visual Basic sind ähnlich. Die Abfrage des letzten Abschnitts

Dim v As IEnumerable(Of Person) = _ 
  daten .OrderBy(Function(a) a.Alter) .ThenBy(Function(a) a.Name)

ist in LINQ genauso aufgebaut, aber noch einfacher in der Formulierung:


'...\Sammlungen\Linq\VBAbfrage.vb

Dim v = From p In daten Order By p.Alter Order By p.Name

Ich habe bewusst ein Beispiel mit einer Umbenennung gewählt, um Sie darauf aufmerksam zu machen, dass nicht immer eine hundertprozentige Korrespondenz besteht. Um etwas mit der Syntax warm zu werden, gehen wir nun den umgekehrten Weg und fangen mit der LINQ-Variante an. Im folgenden Beispiel suchen wir aus einer Liste von Kunden diejenigen heraus, die höchstens vier Teile bestellt haben, und nehmen dann nur ihre Namen. Die Syntax ist in LINQ und direkt darunter mit Erweiterungsmethoden formuliert.


'...\Sammlungen\Linq\LinqZuVB.vb

Option Strict On 
Namespace Sammlungen 
  Module LinqZuVB

    Class Kunde 
      Friend Name As String, Bestellmenge As Integer 
      Sub New(ByVal n As String, ByVal b As Integer) 
        Name = n : Bestellmenge = b 
      End Sub 
    End Class

    Sub Test() 
      Dim daten() As Kunde = New Kunde() {New Kunde("Vogel", 4), _ 
        New Kunde("Li", 9), New Kunde("Maier", 2), New Kunde("Schmidt", 5)}

      Dim kdeLinq = From k In daten Where k.Bestellmenge < 5 Select k.Name

      Dim kdeVB = daten .Where(Function(k) k.Bestellmenge < 5) _ 
                        .Select(Function(k) k.Name)

      For Each p As String In kdeLinq : Console.Write(p & " ") : Next 
      Console.WriteLine() 
      For Each p As String In kdeVB : Console.Write(p & " ") : Next 
      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Beide Abfragen erzeugen eine identische Ausgabe:

Vogel Maier 
Vogel Maier

In LINQ werden zwei Schreibweisen unterschieden:

  • Abfragesyntax (Query-Expression-Syntax)
  • Erweiterungsmethodensyntax (Extension-Method-Syntax)

Letztere ist zwar schwerer zu lesen, schöpft aber die volle Leistungsfähigkeit von LINQ aus. Nicht alle Abfrageausdrücke lassen sich in der Schreibweise der Abfragesyntax ausdrücken. In einigen Fällen kommen Sie an der Erweiterungsmethodensyntax nicht vorbei. Sie können beide Schreibweisen mischen. In jedem Fall wandelt der Compiler die Abfrage in die Erweiterungsmethodensyntax um.


Rheinwerk Computing - Zum Seitenanfang

6.4.4 Abfrageoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

LINQ stellt Ihnen zahlreiche Abfrageoperatoren zur Verfügung. Alle haben korrespondierende Erweiterungsmethoden in der Klasse Enumerable im Namensraum System.Linq. In Tabelle 6.12, »LINQ-Abfrageoperatoren«, sind alle angegeben.


Tabelle 6.12 LINQ-Abfrageoperatoren

Operatortyp Operator

Aggregat

Aggregate, Average, Count, LongCount, Min, Max, Sum

Umwandlung

Cast, OfType, ToArray, ToDictionary, ToList, ToLookup, ToSequence

Element

DefaultIfEmpty, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault

Gleichheit

EqualAll

Sequenz

Empty, Range, Repeat

Gruppierung

GroupBy

Verbindung

Join, GroupJoin

Sortierung

OrderBy, ThenBy, OrderByDescending, ThenByDescending, Reverse

Aufteilung

Skip, SkipWhile, Take, TakeWhile

Quantifizierung

All, Any, Contains

Restriktion

Where

Projektion

Select, SelectMany

Menge

Concat, Distinct, Except, Intersect, Union


Wir werden im weiteren Verlauf des Kapitels auf die meisten der hier aufgeführten LINQ-Operatoren genauer eingehen.


Rheinwerk Computing - Zum Seitenanfang

6.4.5 From-Klausel Zur nächsten ÜberschriftZur vorigen Überschrift

Ein Abfrageausdruck beginnt mit der From-Klausel. Sie beschreibt die abzufragende Datenquelle und definiert eine lokale Bereichsvariable, die jedes Element in der Datenquelle repräsentiert, ähnlich wie die Variable in einer For Each-Schleife. Wie in Abschnitt 6.4.2, »Erweiterungsmethoden«, beschrieben, muss die Datenquelle den Typ IEnumerable(Of T), IQueryable(Of T), IEnumerable oder IQueryable haben oder in einen solchen über fest vorgeschriebene Methoden umwandelbar sein.

Datenquelle und Bereichsvariable sind streng typisiert. Wenn Sie mit

Dim kunden() As Kunde 
From kunde In kunden

das Array aller Kunden als Quelle angeben, ist die Bereichsvariable vom Typ Kunde. Wenn Sie nichtgenerische Auflistungen verwenden, müssen Sie die Bereichsvariable explizit typisieren, denn solche Auflistungen speichern Elemente vom Typ Object. Zum Beispiel:

Dim arr As New ArrayList() 
arr.Add(New Kunde()) 
arr.Add(New Kunde())

Dim cust = From c As Kunde In arr Select Name

Manchmal enthält ein Element der Datenquelle Mitglieder eines strukturierten Typs, wie zum Beispiel das Array im folgenden Typ:

Public Class Bestellung 
  Public Nummer As Integer 
  Public Menge As Integer 
End Class

Public Class Kunde 
  Public Name As String 
  Public Bestellungen() As Bestellung 
End Class

Dim kunden() As Kunde

Jedem Kunden ist ein Array vom Typ Bestellung 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.

Dim p = From k in kunden Where k.Name == "Hans" _ 
           From b in k.Bestellungen Where b.Menge > 6 Select b.Nummer

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.


Rheinwerk Computing - Zum Seitenanfang

6.4.6 Der Restriktionsoperator Where Zur nächsten ÜberschriftZur vorigen Überschrift

Um eine Folge von Elementen zu filtern, verwenden Sie den Where-Operator. Angenommen, Sie möchten alle Flüsse in Deutschland auflisten.

Class Fluss 
  Public Name As String 
  Public Land As String 
End Class

Dim flüsse(3) As Fluß 
flüsse(0) = New Fluss() : flüsse(0).Name = "Elbe" : flüsse(0).Land = "D" 
flüsse(1) = New Fluss() : flüsse(1).Name = "Maas" : flüsse(1).Land = "NL" 
flüsse(2) = New Fluss() : flüsse(2).Name = "Ems" : flüsse(2).Land = "D" 
flüsse(3) = New Fluss() : flüsse(3).Name = "Main" : flüsse(3).Land = "D"

For Each n In From f In flüsse Where f.Land="D" Select f.Name 
  Console.Write(n & " ") 
Next

Mit dem Select-Operator geben Sie das Element an, das in die Ergebnisliste aufgenommen werden soll. In diesem Fall ist das der Name jedes entsprechend dem Where-Operator gefundenen Flusses. Die Ergebnisliste wird in der For Each-Schleife durchlaufen und an der Konsole ausgegeben: Elbe Ems Main.

Sie können die Abfragesyntax auch durch die Erweiterungsmethodensyntax ersetzen. Geben Sie dabei direkt das zu durchlaufende Array an. An der Codierung der Konsolenausgabe ändert sich nichts.

For Each n In flüsse .Where(Function(f) f.Land="D") _ 
                     .Select(Function(f) f.Name)

Um aus einem 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 Land des gefundenen Flusses in der Ergebnisliste, sieht der Code der LINQ-Abfrage wie folgt aus:

For Each n In From f In flüsse Where f.Land="D" _ 
                               Select New With {f.Name, f.Land} 
  Console.Write("{" & n.Name & "," & n.Land & "}") 
Next

Die Ergebnisliste setzt sich aus den objektspezifischen Elementen Name und Land 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 Visual-Basic-spezifischen Operatoren. Im nächsten Codefragment werden alle mit »E« anfangenden Flüsse in Deutschland ausgegeben.

For Each n In From f In flüsse Where f.Land="D" AndAlso f.Name(0)= "E"c _ 
                               Select f.Name 
  Console.Write(n & " ") 
Next

oder:

For Each n In flüsse.Where(Function(f) f.Land="D" AndAlso f.Name(0)= "E"c) _ 
                    .Select(Function(f) f.Name) 
  Console.Write(n & " ") 
Next

Überladungen des Where-Operators

Wenn Sie sich die .NET-Dokumentation des Where-Operators ansehen, finden Sie die beiden folgenden Signaturen:


Public Shared Function Where(Of TSource)( _ 
  source As IEnumerable(Of TSource), _ 
  predicate As Func(Of TSource, Boolean) _ 
) As IEnumerable(Of TSource)

Public Shared Function Where(Of TSource)( _ 
  source As IEnumerable(Of TSource), _
  predicate As Func(Of TSource, Integer, Boolean) _ 
) As IEnumerable(Of TSource)

Die erste wird für Abfragen verwendet, wie wir sie weiter oben eingesetzt haben. Die IEnumerable(Of TSource)-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 des Elements, der als Integer übergeben wird. Wenn nur Flüsse mit ungeradem Index berücksichtigt werden sollen (Maas Main), sieht das so aus:

For Each n In flüsse.Where(Function(f,i) i Mod 2 = 1) _ 
                    .Select(Function(f) f.Name) 
  Console.Write(n & " ") 
Next

Hier müssen Sie die Erweiterungsmethodensyntax einsetzen, um der überladenen Erweiterungsmethode Where die erforderlichen Argumente übergeben zu können.

Funktionsweise des Where-Operators

Betrachten wir noch einmal die folgende Anweisung:

Dim f = flüsse.Where(Function(f) f.Land="D")

var result = customers 
             .Where( cust => cust.City == Cities.Aachen)

Where ist eine Erweiterungsmethode der Schnittstelle IEnumerable(Of T) und gilt auch für das Array vom Typ flüsse. Der Ausdruck

Function(f) f.Land="D"

ist ein Lambda-Ausdruck, im eigentlichen Sinne also das Delegate auf eine anonyme Methode. In der Definition des Where-Operators wird dieses Delegate durch

Func(Of TSource, Boolean)

beschrieben (siehe Definition von Where oben). Der generische Typparameter TSource wird durch die Elemente in der zugrunde liegenden Collection beschrieben, die die Schnittstelle IEnumenerable(Of TSource) implementiert. In unserem Beispiel handelt es sich um Fluss-Objekte. Daher können wir bei korrekter Codierung innerhalb des Lambda-Ausdrucks auch auf die IntelliSense-Liste zurückgreifen. Der zweite Parameter spezifiziert den Typ des Rückgabewerts des Lambda-Ausdrucks. 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.


Rheinwerk Computing - Zum Seitenanfang

6.4.7 Projektionsoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

Select

Der Select-Operator speichert die Ergebnisse der Abfrage in einem Objekt, das die Schnittstelle IEnumerable(Of T) implementiert, zum Beispiel:

Class Kreis : Public X, Y, D As Double : End Class 
Dim kreise() As Kreis 
Dim res = From k in kreise Select k.X

oder alternativ:

Dim res = kreise.Select(Function(k) k.X)

Die Rückgabe ist in beiden Fällen eine Liste mit den x-Positionen der in der Collection vertretenen Kreise.

Liefert der Select-Operator eine Liste mit neu strukturierten Datenzeilen, müssen Sie einen anonymen Typ als Ergebnismenge definieren:

Dim res = From k in kreise Select New With {f.X, f.Y}

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.

Public Class Order : Public Nummer, Menge As Integer : End Class 
Public Class Kunde : Public Name As String, Best() As Order : End Class

Dim kl = From k in kunden Where k.Name = "Hans" _ 
           From b in k.Best Where b.Menge > 6 Select b.Nummer

Weiter oben hatten wir uns bereits mit Untermengen dieser Art beschäftigt. In der Erweiterungsmethodensyntax heißt der Operator SelectMany:

Dim kv = kunden .Where(Function(k) k.Name="Hans") _ 
                .SelectMany(Function(k) k.Best) _ 
                .Where(Function(o) o.Menge > 6) _ 
                .Select(Function(o) o.Nummer)

Rheinwerk Computing - Zum Seitenanfang

6.4.8 Sortieroperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

Sortieroperatoren ermöglichen eine Sortierung von Elementen in Ausgabefolgen mit einer angegebenen Sortierrichtung.

Mit dem Operator Order By können Sie auf- und absteigend sortieren, mit Order By Descending nur absteigend. Hier sehen Sie ein Beispiel für eine aufsteigende Sortierung. Dabei werden die Größen aller Äpfel der Reihe nach in die Ergebnisliste geschrieben.

Class Apfel 
  Public Farbe As String, Größe As Integer, Preis As Double 
  Sub New(ByVal f As String, ByVal g As Integer, ByVal p As Double) 
    Farbe = f : Größe = g : Preis = p 
  End Sub 
End Class

Dim ap() As Apfel = New Apfel() {New Apfel("Rot", 50, 1.3), _ 
  New Apfel("Grün", 70, 1.4), New Apfel("Rot", 60, 1.4), _ 
  New Apfel("Grün", 40, 1.1)}

For Each af In _ 
(From a In ap Order By a.Größe Select New With {a.Größe, a.Preis}) 
  Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

Hinweis
Durch Klammerung der LINQ-Syntax kann sie mit Visual Basic gemischt werden.


Sehen wir uns diese LINQ-Abfrage noch in der Erweiterungsmethodensyntax an:

For Each af In ap.OrderBy(Function(a) a.Größe) _ 
                 .Select(Function(a) New With {a.Größe, a.Preis}) 
  Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

Durch die Ergänzung von Descending bekommen wir eine absteigende Sortierung:

... 
Order By a.Größe Descending 
...

Das folgende Codefragment zeigt, wie Sie mit dem Operator OrderByDescending zum gleichen Ergebnis kommen:

For Each af In ap.OrderByDescending(Function(a) a.Größe) _ 
                 .Select(Function(a) New With {a.Größe, a.Preis}) 
  Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

Wenn Sie mehrere Sortierkriterien festlegen wollen, helfen Ihnen die beiden Operatoren Then By und Then By Descending weiter. Deren Einsatz setzt aber voraus, dass vorher Order By oder Order By Descending verwendet worden sind. Sortieren wir die Äpfel nach ihrem Preis und danach nach ihrer Größe.

Nehmen wir an, die erste Sortierung soll die Bestellmenge berücksichtigen und die zweite, ob die Bestellung bereits ausgeliefert ist. Die Anweisung dazu lautet:

For Each af In ap.OrderBy(Function(a) a.Preis) _ 
                 .ThenBy(Function(a) a.Größe) _ 
                 .Select(Function(a) New With {a.Größe, a.Preis}) 
  Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

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:

For Each af In ap.OrderBy(Function(a) a.Größe) _ 
                 .Select(Function(a) New With {a.Größe, a.Preis}) _ 
                 .Reverse() 
  Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

Wie oben bereits erwähnt wurde, können Sie LINQ- und Visual-Basic-Syntax mischen, wenn Sie die LINQ-Abfrage in runde Klammern setzen. So können Sie Reverse auch dann anwenden.

Wie Sie wissen, werden einige Abfrageoperatoren als Schlüsselwörter von C# angeboten und gestatten die sogenannte Abfragesyntax. Reverse und ThenBy zählen nicht dazu. Möchten Sie die von einer Abfragesyntax gelieferte Ergebnismenge umkehren, können Sie sich eines kleinen Tricks bedienen. Sie schließen die Abfragesyntax in runde Klammern ein und können darauf den Punktoperator mit folgendem Reverse angeben:

For Each af In _ 
  (From a In ap Order By a.Größe Select New With {a.Größe, a.Preis}) _ 
  .Reverse() 
   Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") 
Next

Rheinwerk Computing - Zum Seitenanfang

6.4.9 Gruppieren mit GroupBy Zur nächsten ÜberschriftZur vorigen Überschrift

Manchmal ist es notwendig, Ergebnisse anhand spezifischer Kriterien zu gruppieren. Dazu dient der Operator Group By. Das folgende Beispiel gruppiert Personen nach Geschlecht.


'...\Sammlungen\Linq\GroupBy.vb

Option Strict On 
Namespace Sammlungen 
  Module GroupBy

    Class Person 
      Public Geschlecht, Name As String 
      Sub New(ByVal g As String, ByVal n As String) 
        Geschlecht = g : Name = n 
      End Sub 
    End Class

    Sub Test() 
      Dim pp() As Person = New Person() {New Person("m", "Harry"), _ 
        New Person("w", "Sally"), New Person("w", "Liesl"), _ 
        New Person("m", "Karl")}

      For Each ps As IGrouping(Of String, Person) In _ 
      pp.GroupBy(Function(p) p.Geschlecht) 
        Console.Write("Geschlecht {0}: ", ps.Key) 
        For Each pg In ps 
          Console.Write(pg.Name & " ") 
        Next 
        Console.WriteLine() 
      Next

      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Die Gruppierung ist wie erwartet:

Geschlecht m: Harry Karl 
Geschlecht w: Sally Liesl

Der Operator GroupBy ist vielfach überladen. In der folgenden Syntax sind optionale Parameter kursiv gesetzt:


Public Shared Function GroupBy(Of TSource, TKey, TElement)( _ 
  source As IEnumerable(Of TSource), _ 
  keySelector As Func(Of TSource, TKey), _ 
  elementSelector As Func(Of TSource, TElement), _ 
  comparer As IEqualityComparer(Of TKey) _ 
) As IEnumerable(Of IGrouping(Of TKey, TElement))

Public Shared Function GroupBy(Of TSource, TKey, TElement, TResult)( _ 
  source As IEnumerable(Of TSource), _ 
  keySelector As Func(Of TSource, TKey), _ 
  elementSelector As Func(Of TSource, TElement), _ 
  resultSelector As Func(Of TKey, IEnumerable(Of TElement), TResult), _ 
  comparer As IEqualityComparer(Of TKey) _ 
) As IEnumerable(Of TResult)

Die Schnittstelle IGrouping(Of TKey, TElement) ist eine spezialisierte Form von IEnumerable(Of T) :


Public Interface IGrouping(Of TKey, TElement) 
    Inherits IEnumerable(Of TElement), IEnumerable 
    ReadOnly Property Key As TKey 
End Interface

Betrachten wir nun die äußere Schleife:

For Each ps As IGrouping(Of String, Person) In _ 
               pp.GroupBy(Function(p) p.Geschlecht)

Der Schnittstelle IGrouping weisen Sie im Typparameter den Datentyp des Elements zu, 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 pg erfasst. In der inneren Schleife werden anschließend alle Elemente von ps durchlaufen und die gewünschten Informationen ausgegeben.


Rheinwerk Computing - Zum Seitenanfang

6.4.10 Verknüpfungen mit Join Zur nächsten ÜberschriftZur vorigen Überschrift

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. Im folgenden Beispiel werden die Daten von Produkten und Bestellungen mit gleicher Nummer zusammengefasst.


'...\Sammlungen\Linq\Join.vb

Option Strict On 
Namespace Sammlungen 
  Module Join

    Public Class Produkt 
      Public Nummer As Integer, Name As String 
      Sub New(ByVal n As Integer, ByVal nm As String) 
        Nummer = n : Name = nm 
      End Sub 
    End Class

    Public Class Order 
      Public Nummer, Menge As Integer 
      Sub New(ByVal n As Integer, ByVal m As Integer) 
        Nummer = n : Menge = m 
      End Sub 
    End Class

    Sub Test() 
      Dim pd() As Produkt = New Produkt() { _ 
        New Produkt(22, "Tasse"), New Produkt(34, "Becher")} 
      Dim od() As Order = New Order() {New Order(22, 100), _ 
        New Order(34, 200), New Order(22, 50), New Order(34, 100)}

      For Each det In _ 
      od.Join(pd, Function(o) o.Nummer, Function(p) p.Nummer, _ 
        Function(o, p) New With {p.Nummer, p.Name, o.Menge}) 
        Console.Write("{{{0},{1},{2}}} ", det.Name, det.Nummer, det.Menge) 
      Next

      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Es werden alle Bestellungen mit einem Produktnamen versehen.

{Tasse,22,100} {Becher,34,200} {Tasse,22,50} {Becher,34,100}

Der Join-Operator ist überladen. Der letzte Parameter ist optional:


Public Shared Function Join(Of TOuter, TInner, TKey, TResult)( _ 
  outer As IEnumerable(Of TOuter), _ 
  inner As IEnumerable(Of TInner), _ 
  outerKeySelector As Func(Of TOuter, TKey), _ 
  innerKeySelector As Func(Of TInner, TKey), _ 
  resultSelector As Func(Of TOuter, TInner, TResult), _ 
  comparer As IEqualityComparer(Of TKey) _ 
) As IEnumerable(Of TResult)

Join wird als Erweiterungsmethode der Liste definiert, auf die Join aufgerufen wird. In unserem Beispiel ist es die Liste od aller Bestellungen. Die innere Liste wird durch das Argument beschrieben und ist in unserem Beispielcode die Liste aller Produkte pd. Als zweites Argument erwartet Join im Parameter outerKeySelector das Schlüsselfeld der äußeren Liste (hier: od), das im vierten Argument mit dem Schlüsselfeld der inneren Liste (hier: pd) 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.

Die Schlüssel (in unserem Beispiel werden dazu die Felder genommen), die die Nummer beschreiben, haben den generischen Typ TKey, die Ergebnisliste ist vom Typ TResult.

Sie können eine Join-Abfrage auch in Abfragesyntax notieren:

For Each det In _ 
From o In od Join p In pd On o.Nummer Equals p.Nummer _ 
Select New With {p.Nummer, p.Name, o.Menge} 
  Console.Write("{{{0},{1},{2}}} ", det.Name, det.Nummer, det.Menge) 
Next

Sie sollten darauf achten, dass Sie beim Vergleich links von Equals den Schlüssel der äußeren Liste angeben und rechts davon den der inneren. Wenn Sie beide vertauschen, erhalten Sie einen Compilerfehler.

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 entspricht der Join-Operator dem INNER JOIN-Statement einer SQL-Abfrage.

Das Äquivalent zu einem LEFT OUTER JOIN oder RIGHT OUTER JOIN implementiert der GroupJoin-Operator.

Im folgenden Beispiel werden die Bestellungen aller Kunden nach Produkt gruppiert ausgegeben.


'...\Sammlungen\Linq\GroupJoin.vb

Option Strict On 
Namespace Sammlungen 
  Module GroupJoin

    Public Class Produkt 
      Public Nummer As Integer, Name As String 
      Sub New(ByVal n As Integer, ByVal nm As String) 
        Nummer = n : Name = nm 
      End Sub 
    End Class

    Public Class Order 
      Public Nummer, Menge As Integer 
      Sub New(ByVal n As Integer, ByVal m As Integer) 
        Nummer = n : Menge = m 
      End Sub 
    End Class

    Public Class Kunde 
      Public Name As String, best() As Order 
      Sub New(ByVal n As String, ByVal ParamArray b() As Order) 
        Name = n : best = b 
      End Sub 
    End Class

    Sub Test() 
      Dim pd() As Produkt = New Produkt() { New Produkt(12, "Glas"), _ 
        New Produkt(22, "Tasse"), New Produkt(34, "Becher")} 
      Dim od() As Order = New Order() {New Order(22, 100), _ 
        New Order(34, 200), New Order(22, 50), New Order(34, 100)} 
      Dim kd() As Kunde = New Kunde() {New Kunde("Hans", od(1), od(0)), _ 
        New Kunde("Peter", od(3), od(0)), New Kunde("Willi", od(2))}

      For Each det In _ 
       pd.GroupJoin(kd.SelectMany(Function(k) k.best), _ 
                    Function(p) p.Nummer, Function(o) o.Nummer, _ 
                    Function(p, o) New With {p.Nummer, p.Name, .best = o}) 
        Console.Write("{{{0},{1},{{ ", det.Name, det.Nummer) 
        For Each o In det.best 
          Console.Write("{{{0},{1}}} ", o.Nummer, o.Menge) 
        Next

        Console.WriteLine("}} ") 
      Next 
      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Alle Bestellungen werden erfasst, ebenso das nicht bestellte Produkt:

{Glas,12,{ }} 
{Tasse,22,{ {22,100} {22,100} {22,50} }} 
{Becher,34,{ {34,200} {34,100} }}

GroupJoin arbeitet sehr ähnlich wie der Join-Operator. Der Unterschied zwischen den beiden Operatoren besteht in dem, 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 (siehe die erste Ausgabezeile oben).

Sie können den Group Join-Operator auch in einem Abfrageausdruck beschreiben. Er wird mit Group Join... Into... definiert.

For Each det In 
From p In pd _ 
Group Join c In (From k In kd From o In k.best Select o) _ 
On p.Nummer Equals c.Nummer Into Group _ 
Select New With {p.Nummer, p.Name, .best = Group} 
  Console.Write("{{{0},{1},{{ ", det.Name, det.Nummer) 
  For Each o In det.best 
    Console.Write("{{{0},{1}}} ", o.Nummer, o.Menge) 
  Next 
  Console.WriteLine("}} ") 
Next

Rheinwerk Computing - Zum Seitenanfang

6.4.11 Die Set-Operator-Familie Zur nächsten ÜberschriftZur vorigen Überschrift

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.

Dim cities() As string = New String() { _ 
   "Aachen", "Köln", "Bonn", "Aachen", "Bonn", "Frankfurt"} 
For Each c In (From p In cities Select p).Distinct() 
  Console.Write(c & " ") 
Next

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.

Union

Der Union-Operator verbindet zwei Listen miteinander. Dabei werden doppelte Vorkommen ignoriert.

Dim cities() As String = New String() { _ 
  Aachen", "Bonn", "Aachen", "Frankfurt"} 
Dim namen() As String = New String() {"Peter", "Willi", "Hans"} 
For Each u In cities.Union(namen) 
  Console.Write(u & " ") 
Next

In der Ergebnisliste werden der Reihe nach Aachen, Köln, Bonn, Frankfurt, Peter, Willi und Hans erscheinen.

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.

Dim cities() As String = New String() { _ 
  "Aachen", "Köln", "Bonn", "Aachen", "Frankfurt"} 
Dim namen() As String = New String() { _ 
  "Düsseldorf", "Bonn", "Bremen", "Köln"} 
For Each u In cities.Intersect(namen) 
  Console.Write(u & " ") 
Next

Das Ergebnis wird durch die Städte Köln und Bonn gebildet.

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.

Wenn Sie in dem Codefragment anstelle von Intersect den Operator Except verwenden, enthält die Ergebnisliste die Orte Aachen und Frankfurt.


Rheinwerk Computing - Zum Seitenanfang

6.4.12 Die Familie der Aggregatoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

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 Integer als Typ zurückgibt und LongCount einen Long.

Dim x(7) As Integer 
Console.WriteLine("Anzahl {0}", x.Count()) 
Console.WriteLine("Anzahl {0}", x.LongCount())

Sum

Der Operator liefert eine Summe als Ergebnis der LINQ-Abfrage. Im folgenden Codefragment wird die Summe aller Werte ermittelt, die das Array bilden. Das Ergebnis ist 114.

Dim x() As Integer = New Integer() {1, 3, 7, 4, 99} 
Console.WriteLine("Anzahl {0}", x.Sum())

Das folgende Beispiel ist nicht mehr so einfach. Hier soll der Gesamtbestellwert über alle Produkte für jeden Kunden ermittelt werden.


'...\Sammlungen\Linq\Summe.vb

Option Strict On 
Namespace Sammlungen 
  Module Summe

    Public Class Produkt 
      Public Nummer As Integer, Name As String, Preis As Double 
      Sub New(ByVal n As Integer, ByVal nm As String, ByVal p As Double) 
        Nummer = n : Name = nm : Preis = p 
      End Sub 
    End Class

    Public Class Order 
      Public Nummer, Menge As Integer 
      Sub New(ByVal n As Integer, ByVal m As Integer) 
        Nummer = n : Menge = m 
      End Sub 
    End Class

    Public Class Kunde 
      Public Name As String, best() As Order 
      Sub New(ByVal n As String, ByVal ParamArray b() As Order) 
        Name = n : best = b 
      End Sub 
    End Class

    Sub Test() 
      Dim pd() As Produkt = New Produkt() {New Produkt(12, "Glas", 2.1), _ 
        New Produkt(22, "Tasse", 1.9), New Produkt(34, "Becher", 1.7)} 
      Dim od() As Order = New Order() {New Order(22, 100), _ 
        New Order(34, 200), New Order(22, 50), New Order(34, 100)} 
      Dim kd() As Kunde = New Kunde() {New Kunde("Hans", od(1), od(0)), _ 
        New Kunde("Peter", od(3), od(0)), New Kunde("Willi", od(2))}

      Dim allOrders = From k In kd From o In k.best _ 
        Join p In pd On o.Nummer Equals p.Nummer _ 
        Select New With {k.Name, o.Nummer, .Volumen = o.Menge * p.Preis}

      Dim summe = From k In kd Group Join o In allOrders _ 
        On k.Name Equals o.Name Into Group _ 
        Select New With _ 
          {k.Name, .TotalSumme = Group.Sum(Function(s) s.Volumen)}

      For Each v In summe 
        Console.WriteLine("Name {0} Summe {1}", v.Name, v.TotalSumme) 
      Next 
      Console.ReadLine() 
    End Sub 
  End Module 
End Namespace

Hier ist das Ergebnis:

Name Hans Summe 530 
Name Peter Summe 360 
Name Willi Summe 95

Analysieren wir den Code schrittweise, und überlegen wir, was das Resultat des folgenden Abfrageteilausdrucks ist.

Dim allOrders = From k In kd From o In k.best _ 
  Join p In pd On o.Nummer Equals p.Nummer _ 
  Select New With {k.Name, o.Nummer, .Volumen = o.Menge * p.Preis}

Zuerst ist es notwendig, die Bestellungen aus jedem kd-Objekt zu filtern. Danach wird ein Join gebildet, der die Nummer aus den einzelnen Bestellungen eines Kunden mit der Nummer aus der Liste der Artikel verbindet.

Nun gilt es noch, die Ergebnisliste nach den Kunden zu gruppieren und dann die Gesamtsumme aller Bestellungen zu bilden.

Dim summe = From k In kd Group Join o In allOrders _ 
  On k.Name Equals o.Name Into Group _ 
  Select New With {k.Name, .TotalSumme = Group.Sum(Function(s) s.Volumen)}

Wir sollten uns daran erinnern, dass der GroupJoin-Operator mit diesen Fähigkeiten ausgestattet ist. Es müssen zuerst die beiden Listen kd und allOrders zusammengeführt werden. Sie können sich das so vorstellen, dass die Gruppierung mit GroupJoin zur Folge hat, dass für jeden Kunden 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 Kunde-Objekten gestattet es uns nun, mit dem Operator Sum den Inhalt der Spalte Volumen zu summieren.

Die Operatoren Min, Max und Average

Die Aggregatoperatoren Min und Max ermitteln den minimalen bzw. maximalen Wert in einer Datenliste, und Average ermittelt das arithmetische Mittel.

Grundsätzlich ist der Einsatz der Operatoren sehr einfach, wie das folgende Codefragment exemplarisch an Max zeigt:

Dim 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.

Dim max = (From p in Products Select New With { 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, fehlerverusachenden Codefragment hingegen wird ein anonymer Typ benutzt, der mit der geforderten Schnittstelle überhaupt nicht dienen kann.

Die Lösung dieser 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:

Dim max = (From p in Products Select New With { p.Price }) _ 
          .Max(Function(x) x.Price)

Rheinwerk Computing - Zum Seitenanfang

6.4.13 Generierungsoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

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 Shared Function Range(start As Integer, count As Integer) _ 
As IEnumerable(Of Integer)

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.

Der Range-Operator ist gut geeignet, um mathematische Operationen zu codieren. Dies demonstriert der folgende Code:

Dim nums = Enumerable.Range(1, 10).Select(Function(x) 2 * x) 
For Each num In nums : Console.Write(num & " ") : Next

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 Shared Function Repeat(Of TResult)( _ 
  element As TResult, count As Integer) As IEnumerable(Of TResult)

Dem ersten Parameter übergeben Sie das Element, das wiederholt werden soll. Dem zweiten Parameter teilen Sie die Anzahl der Wiederholungen mit. Mit

For Each s In Enumerable.Repeat("S&N", 3) 
  Console.Write(s & " ") 
Next

wird beispielsweise die Zeichenfolge dreimal ausgegeben.


Rheinwerk Computing - Zum Seitenanfang

6.4.14 Quantifizierungsoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

Wenn Sie beabsichtigen, die Existenz von Elementen in einer Liste anhand von Bedingungen oder definierten Regeln zu überprüfen, helfen die Quantifizierungsoperatoren Ihnen weiter.

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.

Dim res As Boolean = (From cust in Customers From ord in cust.Orders _ 
  Where cust.Name == "Willi" Select New With { ord.ProductID }) _ 
  .Any(ord => ord.ProductID == 7)

Console.WriteLine("ProductID 3 ist {0}enthalten", If(res, "", "nicht "))

Die Elemente werden so lange ausgewertet, bis der Operator auf ein Element stößt, das die Bedingung erfüllt.

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:

Dim res As Boolean = Products.All(Function(p) p.Price > 3)

Rheinwerk Computing - Zum Seitenanfang

6.4.15 Aufteilungsoperatoren Zur nächsten ÜberschriftZur vorigen Überschrift

Mit Where und Select filtern Sie eine Datenquelle nach 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.

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:

Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} 
For Each s In arr.Take(3) : Console.Write(s & " ") : Next

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 Werte in der Ergebnisliste erfasst werden sollen, solange sie positiv sind:

Dim nr() As Integer = New Integer() {3, 8, 3, –2, 7, 1} 
For Each no In nr.TakeWhile(Function(x) x > 0) 
  Console.Write(no & " ") 
Next

Beachten Sie, dass in der Ergebnisliste 7 und 1 fehlen, da die Schleife vorher beendet wird.

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:

Dim res = (From prod in prods _ 
  Select New With { 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.

Dim res = (From prod in prods 
           Select New With { prod.ProductName, prod.Price }) 
           .SkipWhile(Function(x) x.Price > 3 )

Rheinwerk Computing - Zum Seitenanfang

6.4.16 Die Elementoperatoren topZur vorigen Überschrift

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.

First

Dieser Operator sucht das erste Element in einer Datenquelle. Wegen der Überladung kann es sich um das von der Position her 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 werden sieben für das prädikatlose und acht für das prädikatbehaftete Kommando ausgegeben.

Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} 
Console.WriteLine(arr.First()) 
Console.WriteLine(arr.First(Function(x) x(0) < "g"c))

FirstOrDefault

Versuchen Sie einmal, das letzte Codefragment mit dem Prädikat

Function(x) x(0) < "a"c)

auszuführen. Sie werden eine Fehlermeldung erhalten, weil in der Datenquelle kein Element 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 den Standardwert des Datentyps zurück. Handelt es sich um einen Referenztyp, ist das Nothing.

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:

Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} 
Console.WriteLine(arr.FirstOrDefault(Function(x) x(0) < "a"c) Is Nothing)

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.

Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} 
Console.WriteLine(arr.Last()) 
Console.WriteLine(arr.Last(Function(x) x(0) > "g"c))

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, vergleichbar mit der Primärschlüsselspalte einer Datenbanktabelle.

Mit Single und SingleOrDefault prüfen Sie auf eine einelementige Liste. Hat eine Liste mehrere Elemente, wird eine InvalidOperationException ausgelöst. Auch für dieses Pärchen gilt: Besteht die Möglichkeit, dass mehr als ein Element gefunden wird, sollten Sie den Operator SingleOrDefault einsetzen, der – wie bei den anderen Operatoren auch – gegebenenfalls Standardwerte 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.

Dim arr() As String = New String() {"vier"} 
Console.WriteLine(arr.Single()) 
arr = New String() {"sieben", "neun", "acht", "drei"} 
Console.WriteLine(arr.Single(Function(x) x(0) > "o"c))

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.

Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} 
Console.WriteLine(arr.ElementAt(2)) 
Console.WriteLine(arr.ElementAtOrDefault(10) Is Nothing)

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.

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 Ausnahme. Stattdessen ist der Rückgabewert dann entweder der Standardwert oder – falls Sie die überladene Fassung von DefaultIfEmpty eingesetzt haben – ein spezifischer Wert. Im Beispiel wird 77 ausgegeben.

Dim arr(-1) As Integer 
For Each i As Integer In arr.DefaultIfEmpty(77) 
  Console.Write(i & " ") 
Next



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual Basic 2008
Visual Basic 2008
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Visual Basic 2012






 Visual Basic 2012


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de