Did you know that you can navigate the posts by swiping left and right?

Die Dreifaltigkeit in C# 3.0 - λ, LINQ und Extension-Methods (Teil 1 / 3)

17 Jul 2008 . Unknown . Comments

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 dieser Artikelserie möchte ich auf alle drei Themen nacheinander eingehen und so die Zusammenhänge und Abhängigkeiten zwischen ihnen verdeutlichen. Denn bevor man eine neue Technologie auch effektiv einsetzen kann sollte man sie zunächst selber verstehen können.

Obwohl viele Neuerungen sowohl für C# 3.0 als auch Visual Basic .NET 9.0 und 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.

[more]

Teil 1: 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.

Quellennachweise

Questions/Suggestions
As always, for questions or feedback, contact me or leave a comment.

Octocat by GitHubEdit this page on GitHub