.
Anmeldung | Registrieren | Hilfe | Posteingang
Suchen
Home Foren News Member Offers Termine Developer Blogs Knowledge Base

Navigation

Navigationslinks überspringen.
Knowledge Base reduzierenKnowledge Base
Tutorials reduzierenTutorials
Webentwicklung
Cliententwicklung
Datenbankentwicklung
IT Professional
Sharepoint
Sprachspezifisch reduzierenSprachspezifisch
C#
Visual Basic
C++
XAML
SQL
JavaScript
Erfahrungsberichte reduzierenErfahrungsberichte
Entwicklersoftware
Bücher
FAQ Grundlagen

Verknüpfungen

  • Knowledge Base durchsuchen
  • Hilfe zur Knowledge Base
  • RSS Feed
  • Twitter

Grundlagen zu LINQ, λ-Expressions und Extension-Methods

Wenn man von den wichtigsten Spracherweiterungen des .NET Frameworks in der Version 3.5 – und damit auch von Visual C# 2008 – spricht, so denken viele zunächst an LINQ. Beschäftigt man sich mit diesem Thema ein wenig stößt man kurz darauf auf so genannte Lambda-Ausdrücke. Und als ob diese beiden Themen noch nicht umfangreich genug wären basieren beide Technologien auf einer weiteren Neuerung: den Extension Methods.

In diesem Artikel möchte ich auf alle drei Themen nacheinander eingehen um so die Zusammenhänge und Abhängigkeiten zwischen ihnen zu verdeutlichen. Denn bevor man eine neue Technologie auch effektiv einsetzen kann sollte man sie zunächst selber auch verstehen. Ich habe mich bemüht die Erklärungen und Beispiele so zu formulieren, dass sie auch für Anfänger verständlich werden. Für die Grundlagen meiner Meinung nach nicht unbedingt notwendige Bereiche habe ich deshalb weggelassen oder nur verkürzt zusammengefasst.

Obwohl viele Neuerungen sowohl für C# 3.0 als auch Visual Basic .NET 9.0 und bsp. Chrome 2.0 verfügbar sind, werde ich im Folgenden lediglich auf die Implementierung in C# eingehen. Als IDE wurde die englische Version von Microsoft Visual Studio 2008 unter Vista SP1 und Windows XP SP3 eingesetzt. Die Beispiele sind allesamt in C# geschrieben und basieren auf der Version 3.5 des .NET-Frameworks.

 

Extension Methods

Einführung

Die Extension Methods, oder im Deutschen auch Erweiterungsmethoden genannt, sind eine Spracherweiterung die mit der Version 3.5 des .NET-Frameworks eingeführt wurde und es ermöglichen, Datentypen um Funktionalität zu erweitern ohne dass der Entwickler den Quellcode dieser Datentypen besitzen muss. So ließe sich beispielsweise eine Methode ToInt32() für den Datentyp string schreiben, auf den man sonst keinen Zugriff hätte (die string-Klasse ist als sealed markiert). Zunächst wurden die Erweiterungsmethoden allerdings nicht eingeführt um bestehende Datentypen belieb erweitern zu können. Das ist lediglich ein (erfreulicher) Nebeneffekt, den man als Programmierer davon hat. In erster Linie waren die Erweiterungsmethoden für die LINQ-Technologie und die damit verbundenen Lambda-Ausdrücke notwendig geworden, weshalb ich sie hier auch behandeln möchte.

Wie bereits erwähnt und auch am Namen erkennbar kann man andere Datentypen mit den Extension Methods um zusätzliche Funktionalitäten erweitern ohne direkten Zugriff auf den Quellcode dieser Datentypen zu benötigen. Bleiben wir für ein kleines Beispiel beim oben genannten Szenario, einer Methode ToInt32() für die String-Klasse. Eine Erweiterungsmethode wird als statische Methode einer ebenso statischen Klasse deklariert. Der erste Parameter dieser Methode definiert hierbei den zu erweiternen Datentyp und bekommt zur Unterscheidung für den Compiler ein Schlüsselwort vorangestellt (this). Wenn erforderlich können nach diesem (zwingend erforderlichen) Parameter weitere folgen, die beim Aufruf in Klammern übergeben werden können. Ganz allgemein sieht die Deklaration einer Erweiterungsmethode also wie folgt aus:

public static void MeineErweiterung(
       this BestehenderDatentyp parametername[, Parameter]) {
  // Implementierung der Erweiterung
}

Der erste Parameter wird bei der Verwendung der Methode nicht übergeben, innerhalb dieser kann aber sowohl sein Wert als auch die Methoden des jeweiligen Datentyps verwendet werden. Damit haben wir nun alles notwendige an Handwerkszeug um unser Beispiel zu schreiben:

public static class StringExtensions {
  public static int ToInt32( this string s ) {
    int ergebnis;
    if ( Int32.TryParse(s, out ergebnis) ) {
      return ergebnis;
    } else {
      return 0;
    }
  }
}

Extension Methods und null

Wer hat sich noch nicht über eine NullPointerException geärgert, die er vergessen hat abzufangen? In der Tat ist mir aufgefallen, dass die meisten Ausnahmen genau von diesem Typ sind. Das mag daran liegen, dass kaum jemand jedesmal daran denkt bei der Verwendung einer Instanz-Methode vorher das Objekt auf null zu prüfen:

if ( foo != null ) {
  foo.Bar();
}

Lässt man diese Überprüfung weg wird der Aufruf der Methode Bar() vermutlich in 95% der Fälle gut gehen, aber spätestens wenn ein Tester (alternativ der Chef oder Kunde) das Programm startet wird aus einem bisher nicht bedachten Grund das Objekt foo nicht initialisiert sein und schon hat man die gefürchtete Exception. Wie können einem Extension Methods nun hierbei helfen? Durch eine nützliche Eigenschaft: Sie werfen nicht automatisch eine Exception wenn man sie auf ein nicht-initialisiertes Objekt aufruft! Sie sind quasi „geimpft“ und sehen über den Wert null einfach beim Aufruf hinweg:

string foo = "4711";
int bar = foo.ToInt32();
Console.WriteLine( bar );

foo = null;
bar = foo.ToInt32();
Console.WriteLine( bar );

Diese Zeilen laufen ohne das Werfen einer Ausnahme durch und geben „4711“ und „0“ aus, da wir unsere Erweiterungsmethode ToInt32() verwendet haben. Hätten wir dagegen versucht eine Instanzmethode, beispielsweise Trim(), zu verwenden wäre eine Ausnahme geworfen worden. Es bietet sich also in bestimmten Fällen durchaus an, eine Funktion nicht als Instanzmethode sondern als Erweiterungsmethode zu implementieren und sich so bei jeder Verwendung die Überprüfung auf null zu sparen.

Eine mögliche Anwendung dieser Eigenart habe ich bei Chris Brandsma entdeckt und auch wenn er selber nicht ganz davon überzeugt ist („OK, sometimes you have an idea that is one point (seemingly) brilliant, simple, and kind of stupid all in one shot. This is one of those.“) finde ich die Idee durchaus interessant. Im Grunde hat er eine Extension Method geschrieben, die alle Events vom Typ EventHandler<TEventArgs> um eine Methode Fire() erweitert:

using System;
namespace Anheledir.Extensions {
  public static class Events {
    public static void Fire<TEventArgs>(
     this EventHandler<TEventArgs> myEvent,
     object sender, TEventArgs e ) where TEventArgs : EventArgs {
      if ( myEvent != null )
        myEvent( sender, e );
    }
  }
}

Statt nun ein Event direkt zu werfen verwendet man einfach die Erweiterungsmethode Fire() und kann sich dafür jedesmal die Überprüfung sparen ob irgendeine Methode überhaupt an das Event angehängt wurde. Und das kommt schlußendlich auch der Lesbarkeit des Codes zu Gute da man nicht alle paar Zeilen über eine weitere null-Abfrage stolpert. Die Erweiterung des (generischen) Datentyps EventHandler<TEventArgs> ermöglicht die Verwendung dieser Methode für jedes beliebige Event, einfach durch Einbinden des entsprechenden Namensraumes (hier beispielsweise mit: using Anheledir.Extensions;).

Was spricht gegen Extension Methods

Ich habe bisher von zwei Hauptargumenten gegen die Erweiterungsmethoden gelesen und möchte auf diese natürlich ebenso eingehen.

Extension Methods machen den Quellcode schwer lesbar / wartbar

Nehmen wir folgendes Beispiel:

String s = "Mein Hut der hat drei Ecken.";
Console.WriteLine( s.Reverse() );

Was wird mir nun ausgegeben? Eine Möglichkeit wäre: „.nekcE ierd tah red tuH nieM”. Vielleicht aber auch „Ecken drei hat der Hut Mein.”. Das Problem ist, dass man auf den ersten Blick nicht weiß, woher die Methode Reverse() kommt und so auch nicht nachvollziehen kann, was sie eigentlich macht. Das Argument lässt sich aber dank der guten Intellisense Unterstützung in Visual Studio schnell entkräften, da ein Rechtsklick auf die Erweiterungsmethode und die Auswahl von „Go to definition“ einen direkt zum entsprechenden Quellcode führt. Und vom Lesefluß finde ich die Infixnotation der Erweiterungsmethoden auch angenehmer zu lesen als die Präfixnotation:

int p = Summe( 3, 5 ); // Präfixnotation
int i = 3 + 5; // Infixnotation

Extension Methods sind anfälliger für Namespace-Konflikte

Angenommen ich habe zwei Erweiterungsmethoden mit dem gleichen Namen, aber in unterschiedlichen Namensräumen, geschrieben die ich nun mittels using-Direktive in meine Datei einbinde. Oder ich habe eine Erweiterungsmethode geschrieben die den gleichen Namen hat wie eine Instanz-Methode der zu erweiternden Klasse. Oder ich binde in zwei unterschiedlichen Klassen jeweils einen anderen Namensraum ein und die Erweiterungsmethode mit dem gleichen Namen macht jeweils etwas anderes.

Im .NET-Forum wurde in einem Thread gesagt, dass solche Konflikte in der Praxis eher selten wären oder auch bei großen Projekten faktisch gar nicht auftreten. Diese Erfahrung kann ich leider nicht teilen, denn die wenigsten von uns arbeiten in einem reinen Vakuum: Man verwendet APIs oder Erweiterungen von Drittherstellern oder arbeitet in einem größeren Team mit verschiedensten Entwicklern zusammen, zum Teil auch geografisch getrennt. So hat ein Team beispielsweise eine Komponente geschrieben, die in der Anwendung mehrfach referenziert wird. Nach einer gewissen Zeit kommt die Anforderung, dass man diese Komponente um ein paar Methoden erweitert um sie aktuellen Gegebenheiten anzupassen, das ursprüngliche Entwicklerteam sitzt aber gerade an einem anderen Projekt. Man schreibt sich also beispielsweise eine Erweiterungsmethode, die eine dringend benötigte Funktionalität schon mal nachrüstet (man könnte auch von der ursprünglichen Klasse ableiten, aber das Thema ist ja gerade Extension Methods). Eine Weile später wird dann eine gleichnamige Instanz-Methode vom ursprünglichen Entwicklerteam nachgerüstet, die aber ein leicht anderes Ergebnis ausgibt (bsp. ein anders sortiertes Array, …) – die Geschichte lässt sich jetzt sicher noch weiter spinnen. Fakt ist aber, dass man als einziger Programmierer sicher noch darauf achten kann solche Konflikte zu vermeiden, mit zunehmender Anzahl der Entwickler aber auch die Wahrscheinlichkeit eines Namespace-Konfliktes steigt.

Das ist natürlich kein Problem, welches sich nur auf die Erweiterungsmethoden beschränkt sondern vielmehr von grundsätzlicher Natur ist. Die Frage ist hier nur, wie man mit Konflikten im speziellen bei Extension Methods umgehen kann. Im ersten Beispiel ging es darum, dass man in eine Datei zwei Namensräume einbindet die jeweils eine identisch benannte Erweiterungsmethode bereitstellen. Solange man diese nun nicht verwendet tritt auch kein Fehler auf, alleine das Vorhandensein dieses Konfliktes verhindert also noch nicht die Verwendung aller anderen Klassen und Methoden der Namensräume. Erst wenn man die Erweiterungsmethode aktiv benutzen möchte bricht der Kompiler mit einer Fehlermeldung ab, da er die Methode nicht eindeutig zuweisen kann. Das heißt aber auch, dass kein „zufälliges“ oder ungewolltes Verhalten auftreten kann, da bereits der Kompilerfehler die Ausführung des Programms verhindert. Im zweiten Fall hatte die Erweiterungsmethode den gleichen Namen wie eine Instanz-Methode. Hier gibt es eine einfache Regel: Instanz-Methoden haben immer Vorrang! Das hat im übrigen auch den Vorteil, dass ich eine gleichnamige Instanz-Methode auch dann aufrufen kann, wenn es mehrere Erweiterungsmethoden mit dem gleichen Namen geben würde (was ohne die Instanz-Methode wie bereits erwähnt zu einem Fehler beim Kompilieren führen würde).

Im Notfall kann man das Problem der Namenskonflikte außerdem umgehen, in dem man die Erweiterungsmehode wie eine „normale“ statische Methode aufruft:

string s = "4711";
int i = StringExtensions.ToInt32( s );

Fazit

Wie schwer jeder Programmierer diese Gründe gewichtet ist selbstverständlich jedem selber überlassen. Jedoch sind Extension Methods lediglich der Zuckerguss, der den Quellcode schöner / besser lesbar machen kann und vielleicht auch in den ein oder anderen Situationen einem Arbeit abnimmt. Dabei sollte man es natürlich nicht übertreiben, die Instanz-Methoden sind in vielen Fällen immer noch zu bevorzugen. Nur weil man ein neues „Spielzeug“ hat muss man es ja nicht gleich immer und überall auch verwenden.

 

Lambda(λ)-Expressions

Nachdem wir nun ausführlich über die Erweiterungsmethoden gesprochen haben, können wir uns den so genannten Lambda-Expressions oder -Ausdrücken widmen. Sie sind lediglich eine andere Schreibweise für anonyme Methoden wie man sie bereits bei der Verwendung von Delegaten einsetzen konnte. Die Entscheidung eine Methode anonym zu erstellen sollte immer dann gefällt werden, wenn sie eigentlich zu „unwichtig“ für einen eigenen Namen ist (da man sie beispielsweise nur einmal benötigt). Die Schreibweise eines solchen Ausdrucks ist auf den ersten Blick etwas komplex; sobald man aber die Idee dahinter verinnerlicht hat, sollte sie kein Problem mehr darstellen.

In den folgenden Beispielen werde ich eine Liste vom Typ „Person“ aus dem Buch „Visual C# 2008“
verwenden, die mit einigen Beispieldaten gefüllt ist:
public class Person {
  #region Automatisch implementierte Eigenschaften
  public string Title { get; set; }
  public string LastName { get; set; }
  public string FirstName { get; set; }
  public string Street { get; set; }
  public string Zip { get; set; }
  public string City { get; set; }
  public int Age { get; set; }
  #endregion

  #region Methode ToString()
  public override string ToString() {
    StringBuilder result = new StringBuilder();
    result.Append( Title );
    result.Append( " " );
    result.Append( FirstName );
    result.Append( " " );
    result.Append( LastName );
    result.Append( ", " );
    result.AppendFormat( "Alter: {0}, ", Age );
    result.Append( Street );
    result.Append( ", " );
    result.Append( Zip );
    result.Append( " " );
    result.Append( City );
    return result.ToString();
  }
  #endregion
}

Einführung

Springen wir also gleich ins kalte Wasser mit einem Beispiel, in dem wir nur die Personen aus der Liste wissen wollen, deren Nachname mit einem „P“ beginnt. Bisher musste man dafür ein ähnliches Konstrukt wie das folgende verwenden:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<Person> Ergebnis = null;
foreach ( Person p in Adressbuch ) {
  if ( p.LastName.StartsWith( "P" ) ) {
    Ergebnis.Add( p );
  }
}

Das gleiche Ergebnis lässt sich nun mit Lambda-Expressions wie folgt erreichen:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<Person> Ergebnis = Adressbuch.Where( 
             p => p.LastName.StartsWith( "P" ) );

Was ist hier geschehen? Zum Verständnis dieser Abfrage ist die Funktionsweise der Erweiterungsmethode Where() aus dem Namensraum System.Linq wichtig. Die genaue Implementierung von Where() in diesem speziellen Fall ist etwas komplexer, im Wesentlichen handelt es sich aber um die folgende Methode:

public static IEnumerable<T> Where<T>( this IEnumerable<T>
                       source, Func<T, bool> predicate ) {
  foreach ( T item in source ) {
    if ( predicate( item ) ) {
      yield return item;
    }
  }
}

Wir haben hier also eine Erweiterungsmethode vor uns die man auf alle Elemente anwenden kann, welche das Interface IEnumerable<T> implementieren (der erster Parameter mit dem Schlüsselwort this). Außerdem hat sie eine Rückgabe vom gleichen Typ und benötigt einen Übergabeparameter. Bei letztgenannten handelt es sich um einen Delegaten, das heißt es wird an dieser Stelle eine Funktion erwartet die als Übergabeparameter den Typ T entgegen nimmt und einen boolschen Wert als Ergebnis zurückgibt.

Werfen wir einen Blick in den Methodenrumpf um den Aufruf besser zu verstehen. Hier wird in Zeile 2 zunächst jedes Element der ursprünglichen Liste durchlaufen. Wir erinnern uns: Innerhalb einer Erweiterungsmethode kann man auf den Datentyp und seine Werte über den beim Schlüsselwort this angegebenen Namen zugreifen, hier also source. Innerhalb unserer foreach-Schleife haben wir nun die Variabel item vom Typ T (in unserem Beispiel ist T der Typ Person). In der 3. Zeile kommt nun unser Delegate mit dem Namen predicate zum Einsatz: Wir übergeben der beim Aufruf angegebenen Methode unser aktuelles Objekt item und erhalten als Ergebnis entweder true oder false. Die Erweiterungsmethode where soll ja nur die Elemente zurückgeben für die die Bedingung erfüllt ist, deshalb benutzen wir noch die if-Abfrage und geben das aktuelle Objekt nur dann zurück wenn der Aufruf der über predicate referenzierten Methode true zurückgibt. Das Schlüsselwort yield in Zeile 4 speichert die interne Laufvariabel der foreach-Schleife zwischen um beim erneuten Aufruf nicht an den Anfang zu springen, sondern vielmehr hinter die durch yield festgelegte Position innerhalb der Liste.

Kommen wir zu unserem Beispiel-Aufruf zurück. Hier fällt uns als nächstes das Zeichen => auf. Hierbei handelt es sich um den so genannten Lambda-Operator. Man liest ihn als „geht zu“ bzw. im Englischen als „goes to“. Vor dem Lambda-Operator steht der zu übergebene Parameter. Den Datentyp lässt man in der Regel weg, da er bereits durch die verwendete Liste bekannt ist. Der Name ist frei wählbar bzw. unterliegt den üblichen Konventionen für Variabelnnamen. Auf der rechten Seite des Lambda-Operators steht der Code, der in der anonymen Methode ausgeführt werden soll. Das Schlüsselwort return läßt man hier weg, da es beim Aufruf implizit aufgerufen wird. Da unser Lambda-Ausdruck nur eine verkürzte Schreibweise für eine anonyme Methode ist, gibt es natürlich auch noch eine alternative Schreibweise wie man sie so auch mit dem .NET-Framework in der Version 2.0 hätte schreiben können:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<Person> Ergebnis = Adressbuch.Where( delegate( Person p ) {
        return p.LastName.StartsWith( "P" );
      });

Diese Schreibweise ist allerdings nicht so schön lesbar und kompakt wie die Lambda-Expression obwohl sie genau das gleiche macht. Der neue Syntax ist hier deutlich überlegen und sollte deshalb auch bevorzugt werden.

Steuern der Rückgabe / Projektionen

Als Rückgabewert haben wir bisher immer eine Liste vom gleichen Datentyp gehabt wie den der Ursprünglichen Liste. Das funktioniert auch ohne zusätzliche Befehle problemlos, aber manchmal benötigt man als Ergebnis gar nicht das vollständige Objekt oder möchte beispielsweise einen zusätzlichen Wert haben, der erst berechnet werden muss. Für diese Fälle kann man das Ergebnis unseres Lambda-Ausdrucks auch verändern, man spricht in diesem Fall von einer Projektion auf einen neuen Datentypen. Veranschaulichen wir das ebenfalls an einem kleinen Beispiel:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<string> Ergebnis = Adressbuch.Select( p => p.FirstName 
                 + " " + p.LastName + " (" + p.Age + ")" );

Wie man am Datentyp des Lambda-Ausdrucks bereits sieht erhalten wir dieses mal keine Liste mit Objekten des Typs Person sondern lediglich eine Liste mit Zeichenketten. Jeder Einzelne Eintrag besteht aus dem Vor- und Nachnamen einer Person und deren Alter dahinter in runden Klammern. Diese Liste mit Objekten vom Typ string tritt somit an die Stelle des eigentlich verwendeten Datentyps, die ursprünglichen Daten werden quasi auf einen neuen Typ projeziert.

Für das nächste Beispiel greife ich auf ein weiteres neues Feature von C# 3.0 zurück, die so genannten anonymen Datentypen. Es handelt sich hierbei analog zu den anonymen Methoden um einen Datentyp, den man beispielsweise nur an einer Stelle benötigt und für den man deshalb keine eigene Klasse schreiben möchte. Die Verwendung ergibt sich aus dem Beispiel, für nähere Informationen zu anonymen Datentypen empfehle ich den Artikel „Anonyme Typen“ im C#-Programmierhandbuch.

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
var Ergebnis = Adressbuch.Where( p => p.LastName.StartsWith( "P" ) )
                                  .Select( p => new {
                                   Vorname = p.FirstName;
                                   Nachname = p.LastName;
                                   Geburtsjahr = DateTime.Now.Year - p.Age;
                                   Weiblich = p.Title == "Frau"
                                 } );

Der Zeilenumbruch und die Einrückung sind rein optischer Natur. Man könnte sie auch weglassen und den Ausdruck in eine einzige Zeile schreiben. Eine saubere Formatierung macht es allerdings einfacher den Überblick zu wahren und man erkennt die einzelnen Operatoren auf einen Blick. Die Berechnung des Geburtsjahres ist nicht ganz sauber da das genaue Datum außer acht gelassen wird. Die Eigenschaft soll hier lediglich als Beispiel für eine „berechnete“ Eigenschaft dienen.

Als Ergebnis des obigen Beispiels erhält man eine Variabel Ergebnis mit unbekanntem Datentyp, allerdings spielt dieser Datentyp auch keine große Rolle da wir mit diesem nun einfach arbeiten können. Visual Studio 2008 unterstützt uns hierbei mit Intellisense, so dass wir beispielsweise über Ergebnis.Weiblich abfragen können, ob es sich bei der aktuellen Person um eine Frau (true) oder einen Mann (false) handelt.

Wie man im letzten Beispiel sieht kann man die Erweiterungsmethoden natürlich auch kombinieren, im Beispiel die Bedingung (Where) mit einer Auswahl / Projektion (Select). Neben diesen beiden Erweiterungsmethoden gibt es noch zahlreiche andere, von denen ich im Folgenden ein paar wichtige kurz vorstellen möchte.

Erweiterungsmethoden für λ-Ausdrücke im Namensraum System.Linq

Sortieren

Mit Hilfe der Erweiterungsmethode OrderBy() können die einzelnen Elemente in eine neue Reihenfolge gebracht werden. Als Parameter übergibt man die Eigenschaft nach der die Sortierung erfolgen soll. Bei der Verwendung von OrderBy() wird immer aufsteigend (ascending) sortiert. Möchte man die Reihenfolge umdrehen, also absteigend (descending) sortieren, verwendet man stattdessen die Methode OrderByDescending().

Für die Sortierung nach mehreren Kriterien, beispielsweise erst nach Alter und dann nach Nachname, gibt es weitere Erweiterungsmethoden: ThenBy() bzw. ThenByDescending().

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
var Ergebnis = Adressbuch.OrderByDescending( p => p.Age )
                         .ThenBy( p => p.LastName )
                         .ThenBy( p => p.FirstName );

Berechnungen

Es gibt fünf vordefinierte Methoden um Berechnungen durchzuführen: Count(), Average(), Sum(), Min() und Max(). Die Funktionsweise erschließt sich denke ich aus dem jeweiligen Namen, so dass wir direkt ein Beispiel einschieben bei dem man auch den Datentyp der Rückgabe sieht:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );

double Durchschnittsalter = Adressbuch.Average( p => p.Age );
int Gesamtalter           = Adressbuch.Sum( p => p.Age );
int Hoechstalter          = Adressbuch.Max( p => p.Age );
int JuengstesAlter        = Adressbuch.Min( p => p.Age );
int AnzahlTeenager        = Adressbuch.Count( p => p.Age < 19 );

Neben diesen vordefinierten Methoden gibt es allerdings auch noch eine allgemeine Aggregatfunktion die passenderweise Aggregate() heißt und gleich mehrfach überladen ist. In einer einfachen Variante wird als erster Parameter ein Startwert erwartet, der für die folgenden Berechnungen herangezogen wird. Dementsprechend gibt diese Erweiterungsmethode auch den gleichen Datentyp zurück wie der dieses Startwertes. Als zweiten Parameter erwartet die Methode das jeweils nächste Element der Liste (in unserem Beispiel also vom Typ Person). Wir wollen nun in einem Beispiel die Methode Sum() nachbilden:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
int GesamtalterMitAggregat = Adressbuch.Aggregate( 0, (
	     ergebnis, naechster ) => ergebnis += naechster.Age );

Der Startwert wurde hier auf 0 gesetzt, ergebnis ist folglich vom Datentyp int. Die Variabel naechster ist vom Typ Person und stellt jeweils das nächste Element der Liste da. Innerhalb unserer Lambda-Expression sind beliebige Berechnungen möglich so daß die Methode Aggregat() ein enormes Potential bietet.

Wiederholungen löschen

Bei einigen Abfragen gilt es redundante Informationen aus dem Ergebnis auszufiltern. Wenn wir beispielsweise alle Orte wissen wollen, in denen Personen aus unserem Adressbuch leben, dann würden viele Orte mehrfach auftauchen. Um die unnötigen Wiederholungen aus der Ergebnisliste zu löschen kann man die Methode Distinct() verwenden, die die meißten vermutlich auch aus dem Sprachrepertoire von SQL kennen. Um die doppelten Einträge rauszufinden verwendet die Erweiterungsmethode ohne Übergabeparameter den Standartvergleich, sofern ein solcher existiert. Alternativ kann man auch als Parameter ein Objekt übergeben, das das generische Interface IEqualityComparer<T> implementiert. Zu diesem gehören die beiden Methoden Equals() und GetHashCode(), die von Distinct() auch beide für den Vergleich herangezogen werden. Möchte man also nur eine der beiden Implementierungen verwenden, kann die jeweils andere Methode immer true (oder einen beliebigen anderen, aber immer identischen Wert) zurückgeben. Um nun also eine Liste mit allen Wohnorten zu erhalten müssen wir zuvor eine eigene Implementierung von IEqualityComparer schreiben. Dabei können wir natürlich auf bereits implementierte Vergleichsmethoden, beispielsweise von der Klasse string, zurückgreifen:

class WohnortVergleich : IEqualityComparer<Person> {
  public bool Equals( Person a, Person b ) {
    return a.City.Equals( b.City );
  }

  public int GetHashCode( Person p ) {
    return Convert.ToInt32( p.ZIP );
    // Alternativ: return -1;
  }
}

Die eigentliche Ausgabe unter Verwendung von Distinct() kann dann wiefolgt realisiert werden:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<string> Wohnorte = Adressbuch.Distinct( new WohnortVergleich() )
                                  .Select( p => p.City );

Weitere Erweiterungsmethoden

Mit den wenigen hier vorgestellten Erweiterungsmethoden aus dem Namensraum System.Linq hat man noch lange nicht alle Möglichkeiten ausgeschöpft. So gibt es beispielsweise noch Methoden zur Quantifizierung (All(), Any(), Contains()), zur Gruppierung von Abfragen (GroupBy()) und zur Verknüpfung mehrerer Listen (Join()) die ich hier aber nicht alle im Detail vorstellen kann. Hier verweise ich auf andere Artikel und die msdn C#-Sprachreferenz.

Fazit

Das Prinzip, das hinter den Lambda-Ausdrücken steckt, ist nicht ganz neu. Auch in C# 2.0 kannte man Delegaten und konnte mit entsprechenden Aufwand Listen sortieren und filtern. Die Lambda-Ausdrücke reduzieren die notwendige Tipparbeit aber auf ein Minimum und sind vom Syntax mit den zahlreichen Erweiterungsmethoden deutlich vereinfacht worden. So muß man sich keine Gedanken mehr darum machen, wie man eine Sortierung für eine Liste implementiert sondern verwendet einfach Methoden die an die gängigen Bezeichnungen des SQL-Syntax angelehnt sind und dem Programmierer so das Leben ein gutes Stück einfacher machen.

In diesem zweiten Abschnitt wurde auch deutlich wieso die Einführung der Erweiterungsmethoden notwendig geworden ist. Erst in der Kombination von Erweiterungsmethode und der dazugehörigen anonymen Methode können die Lambda-Ausdrücke ihr volles Potential entfalten. Das heißt aber nicht, das sie auf diese Abfragen beschränkt sind. Überall wo ein Delegat zum Einsatz kommt, beispielsweise bei einem Event, lassen sich die anonymen Methoden auch mittels einer Lambda-Expression beschreiben. Sie sind deshalb eine Bereicherung für jeden Programmierer wenn man sich denn zuvor auf das Konzept, das dahinter steht, einlässt.

 

LINQ

Nachdem wir nun eine Menge Vorarbeit geleistet haben wird es nunmehr Zeit die letzte Etappe dieses Einführungsartikels zu beschreiten: Das Language Integrated Query, kurz LINQ. Ohne Übertreibung kann man sagen, dass LINQ eines der umfangreichsten Sprach-Features der letzten Jahre im .NET-Framework ist und über kein Thema wurde seit Veröffentlichung so kontrovers diskutiert. LINQ bietet eine an den SQL-Syntax angelehnte Abfragemöglichkeit auf Listen an.

Nun haben wir ja bereits im vorherigen Abschnitt gelesen, dass man das auch mit den neuen Lambda-Expressions kann und intern werden alle LINQ-Queries auch in Lambda-Ausdrücke umgewandelt! Allerdings sind diese Abfragen dank des an SQL angelehnten Syntax leichter zu verstehen wenngleich auch nicht so mächtig wie die Lambda-Ausdrücke. So gibt es beispielsweise für das Zusammenfügen mehrerer Listen mittels Concat() kein Äquivalent in LINQ und auch eine Entsprechung für die Aggregatfunktionen wie Count(), Min() oder Max() fehlt.

Es wurde auch die Möglichkeit implementiert LINQ auf Daten anzusetzen so dass Abfragen auf beispielsweise XML, DataSets oder SQL-Datenbanken möglich sind. Hier bietet einem LINQ sogar in der Datenbankvariante (LINQ to SQL) einen O/R-Mapper. Mit den sich hieraus ergebenden vielfältigen Möglichkeiten kann man ganze Bücher füllen, weswegen ich mich hier auf die Grundlagen von LINQ beim Zugriff auf Listen beschränken werde. Für weiterführende Informationen verweise ich auf entsprechende Fachliteratur.

Einführung

Greifen wir noch einmal auf unser erstes Beispiel mit Lambda-Ausdrücken zurück, in dem wir alle Personen ausgelesen haben deren Nachname mit einem „P“ beginnt und formulieren das dieses mal als LINQ-Query:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
List<person> Ergebnis = from person in this.Adressbuch
                        where person.LastName.StartsWith( "P" )
                        select person;

Gehen wir auch diesen Ausdruck Schritt für Schritt durch: In Zeile 2 wird definiert auf welche Liste die LINQ-Abfrage ausgeführt werden soll (this.Adressbuch) und wie wir den Parameter nennen wollen, der ein einzelnes Element dieser Liste repräsentiert (person). Der Datentyp dieses Parameters muss hier nicht angegeben werden da er bereits hinreichend durch den Datentyp der Liste festgelegt ist.

Um nun unser Vergleichskriterium festzulegen verwenden wir die where-Klausel. Hierbei werden wir in Visual Studio 2008 von IntelliSense unterstützt. Als Ergebnis dieser Klausel erhalten wir eine Liste vom Datentyp IEnumerable<Person>. Um nun festzulegen was wir vom LINQ-Query zurückgeliefert bekommen wird die select-Klausel verwendet; in unserem Beispiel also eine Sequenz von person-Objekten.

Es fällt auf, dass im Gegensatz zum bekannten SQL-Syntax die Reihenfolge der einzelnen Befehle etwas verdreht ist. So wird zunächst am Anfang definiert, was unsere Datenquelle ist und darauf dann diverse Operationen (wie bsp. Filtern oder Sortieren) angewendet. Erst am Schluß wird dann festgelegt in welcher Form diese Daten ausgegeben werden sollen. Diese Reihenfolge entspricht auch eher unserer Leserichtung. Es sei weiterhin angemerkt, dass der Kompiler die Schlüsselworte wie from und select nur innerhalb eines solchen LINQ-Queries interpretiert. Alter Code, in dem beispielsweise eine Variabel from genannt wurde, funktioniert also auch weiterhin; man sollte aber dennoch künftig auf eine solche Benennung zu Gunsten der Übersicht verzichten um Verwechslungen zu vermeiden.

Die Operatoren

LINQ besitzt im Vergleich zu den Lambda-Expressions nur wenige Operatoren. Das bedeutet im Umkehrschluß das man zwar jedes LINQ-Query auch als Lambda-Expression wiedergeben kann, umgekehrt jedoch (wie bereits erwähnt) nicht. Im folgenden werde ich die unterstützten Operatoren kurz mit ihrer jeweiligen Funktion vorstellen.

from

Wie bereits erwähnt wird in der from-Klausel die Datenquelle angegeben, auf welche die komplette Abfrage angewendet wird. Sie definiert also die Bereichsvariabel, die wir bei Lambda-Expressions vor dem Lambda-Operator ( => ) gefunden haben.

where

Um die Ergebnisse unserer Abfrage zu filtern wird dieser Operator verwendet. Mehrere Bedingungen können wie in C# üblich mit den Operatoren && (logisches UND) bzw. || (logisches ODER) verknüpft werden.

select

Mittels der select-Klausel wird die Rückgabe des LINQ-Queries definiert. Sie entspricht somit der Select()-Anweisung bei den λ-Ausdrücken. Dem entsprechend ist es auch mit diesem Operator möglich entweder das ganze Element zurückzuliefern (bsp. person) oder entweder einen anderen bereits existierenden oder auch anonymen Datentyp mittels Projektion.

orderby

Mit Hilfe dieses Operators lässt sich die Ausgabe nach einem oder mehreren Kriterien sortieren. Hierbei werden ebenfalls die Schlüsselworte für eine aufsteigende (ascending, der Standard) bzw. eine absteigende (descending) Sortierung unterstützt: orderby person.LastName, person.Age descending (zunächst eine aufsteigende Sortierung nach dem Nachnamen, anschließend absteigend nach dem Alter der jeweiligen Personen).

group

Um meine Ergebnisse nach bestimmten Kriterien zu gruppieren kann man die group-Klausel verwenden. Sie entspricht in ihrer Funktion der GroupBy()-Methode, auf die ich bei den Lambda-Ausdrücken nicht näher eingegangen bin. Kurz zusammengefasst kann man sagen, dass das Resultat eine Liste aus IGrouping<K, V> Elementen ist. Der mit K bezeichnete Datentyp ist der Schlüssel, nach dem gruppiert wurde, und der mit V bezeichnete Datentyp ist der enthaltene Wert – also beispielsweise eine Liste mit person-Objekten. Zur Veranschaulichung ein einfaches Beispiel, bei dem wir das Resultat nach Postleitzahlen gruppieren:

List<Person> Adressbuch = PersonFactory.CreateRandomPeople( 100 );
var Ergebnis = from item in this.Adressbuch
               group item by item.PLZ into ausgabe
               select ausgabe;

Wir haben hier außerdem noch das Schlüsselwort into verwendet, um das Ergebnis der Gruppierung in einer temporären „Variabeln“ zu speichern damit wir später erneut bei der select-Klausel darauf zurückgreifen können.

let

Um weitere Unterabfragen innerhalb eines einzelnen Queries zu machen müssen die Abfrageergebnisse mit dem Operator let temporär zwischengespeichert werden um sie verwenden zu können. Man verwendet ihn in der Regel um Unterabfragen (in SQL auch „Subselects“ genannt) zu verknüpfen, für die sich der join-Operator nicht eignet (siehe unten) und wird hier nur der Vollständigkeit halber aufgeführt.

join

Genau wie die Methode Join() bei Lambda-Ausdrücken dient die join-Klausel dem Zusammenfügen mehrerer Ergebnisse zu einer resultierenden Liste, einen so genannten Equijoin. Hiermit bezeichnet man die Verknüpfung zweier Listen und nimmt als Grundlage die Gleichheit jeweils ein Elementes aus jeder Liste (bsp. eine ID). Diese Art von Vergleich ist vor allem in relationalen Datenbanken in der 2. oder 3. Ableitung häufig zu finden.

Fazit

Zunächst freut es mich, dass Sie bis hierher gekommen sind! Neben den relativ wenigen hier exemplarisch aufgeführten Punkten gibt es noch eine vielzahl weiterer Anwendungszwecke die für den geneigten Programmierer sicherlich interessant sind. Mein Ziel war es jedoch lediglich die rudimentären Grundlagen zu liefern und für das nötige Hintergrundwissen zu sorgen damit man überhaupt versteht was passiert wenn man eine LINQ-Abfrage startet. Ich hoffe das ich die hier erwähnten neuen Technologien von .NET 3.5 – allen voran natürlich LINQ – dem einen oder anderen schmackhaft machen konnte. Einige Bereiche habe ich dabei bewußt nur kurz angeschnitten um Einsteiger nicht hoffnungslos zu überfordern und interessierten Lesern einen Anreiz auf weitere Möglichkeiten zu bieten. Auf der Basis dieser Grundlagen kann man nun weiter experimentieren und sich mit den zahlreichen Erweiterungen (LINQ to SQL, LINQ to XML, LINQ to Amazon,  …) vertraut machen die zwischenzeitlich entwickelt wurden.

Programm-Tipp

Wer gerne ein wenig mit LINQ-Abfragen oder Lambda-Ausdrücken experimentieren möchte dem empfehle ich das kostenlose Programm LINQPad von Joseph Albahari. Das Programm ist recht klein und kommt als eine einzelne ausführbare Datei ohne eigene Installation aus – lädt also geradezu zum experimentieren ein. Ein Must-Have für jeden, der sich für LINQ & co. interessiert!

Buch-Tipp

Im Addison Wesley Verlag ist im Juni 2008 das Buch Visual C# 2008 von Frank Eller erschienen (ISBN: 978-3-8273-2641-6). Mit seinen rund 1.300 Seiten und einem Preis von 49,95 Euro ist es sicher keine Lektüre für zwischendurch. Es wird jedoch auf sowohl die Grundlagen als auch die fortgeschrittenen Funktionen von C# 3.0 eingegangen. Allerdings empfehle ich dem gewillten Leser bereits ein wenig Erfahrung in Bezug auf Objektorientierung und die Programmierung im allgemeinen (bsp. mit classic ASP oder auch Java). Neueinsteiger ohne jede Vorkenntnis haben vor allem am Anfang eine recht hohe Lernkurve bei diesem Buch. Einsteiger mit Vorkenntnissen, Umsteiger und fortgeschrittene Programmierer erhalten jedoch ein Praxis- und Nachschlagebuch, das sie bei der täglichen Arbeit mit C# begleitet (so auf dem Buchrücken formuliert). Aus diesem Buch sind auch ein paar der in diesem Artikel genannten Beispiele entlehnt.

von Anheledir, 29.07.2008 zugeordnet zu C# .

Kommentare

Bei mir werden Scrolbars im Artikel selbst angezeigt.

Ist es möglich ihn so zu formatieren, dass diese nicht mehr angezeigt werde. Ist zum lesen wesentlich angenehmer.
von frekrae, 29.07.2008.

Sorry, hatte nicht daran gedacht, dass mein verwendetes Plugin für die Codeformatierung hier (noch) nicht unterstützt wird. Inzwischen ist es aber auch ohne horizontales Scrollen lesbar. :)
von Anheledir, 03.08.2008.

Eigener Kommentar

Sie müssen angemeldet sein, um ein Kommentar zu erstellen.
  • Schwierigkeit: Einsteiger
  • Views: 6395
  • Zur Druckversion
  • Artikel von Anheledir

Kick it on dotnet-kicks.de

Artikel

Autor

Kick it!

Wenn ihnen dieser Artikel gefällt, bitte "kicken" sie ihn.

WPF Forum | ASP.NET Forum | ASP.NET MVC Forum | Silverlight Forum | Windows Phone 7 Forum | SharePoint Forum | Dotnet Jobs | Dotnet Termine | Developer Blogs | Dotnet News

Das Team | Regeln | Impressum