Das ist neu in C# 2.0

21.04.2006 von Thomas Wölfer
Mit C# 2.0 kommen auch neue Funktionen. Die zentralen neuen Sprachelemente sind dabei Generics, Anonymous Methods, Iterators, und Partial Types. In diesem Beitrag erfahren Sie, wie Sie die neuen Möglichkeiten der Sprache effizient nutzen.

Bei C# (sprich: "See Sharp") handelt es sich um eine einfache, objektorientierte und typensichere Sprache, die ihre Wurzeln in C und C++ hat. Programmierer, die mit C, C++ oder Java arbeiten, werden nur wenige Probleme haben, sich in der neuen Sprache wohl zu fühlen. Da C# die primäre Programmiersprache für .NET ist und nicht nur für Windows-Client-Anwendungen, sondern auch für Server-Programme und ASP.NET-Webseiten verwendet werden kann, bietet sich die Sprache als neues Werkzeug im Programmierer-Werkzeugkasten um so mehr an.

C# ist leicht zu haben: Man benötigt nicht unbedingt ein MSDN-Abo oder eine Kopie von Visual Studio, um damit zu arbeiten, denn das kostenlose .NET-SDK enthält bereits einen Compiler für diese Sprache. Der hat zwar keine eigene Entwicklungsumgebung und ist damit etwas unhandlich zu bedienen, dafür ist er aber kostenlos.

Generics

Generics ermöglicht es, Klassen, Structs, Interfaces, Delegates und Methoden mit dem Typ, den sie speichern oder manipulieren, zu parametrisieren. Damit verschaffen die Generics der Sprache zum einen bessere Ausdrucksmöglichkeiten schaffen, während sie gleichzeitig effektiveren Code ermöglichen.

Generics bringen ein besseres Compile-Time Typechecking mit sich und führen dazu, das Sie weniger Typecasts durchführen müssen. Ebenso verringert der Einsatz von Generics die boxing-Operationen, Bedarf an Typechecks zur Laufzeit geht ebenfalls zurück. Mit einem Wort: Generics sind einfach wunderbar.

Einsatz generischer Collections

Generics spielen ihre volle Stärke erst in Collections aus. Microsoft trägt dem Rechnung und liefert in der neuen BCL (Base Class Library) gleich einen ganzen Satz an generischen Collections mit.

Die neuen generischen Collections finden Sie diese im Namespace System.Collections.Generics. Eine der dort vorliegenden Klassen ist zum Beispiel List<T>. Dabei handelt es sich um eine generische Listenklasse, also eine typisierte Version der alten "ArrayList". Der Typ-Parameter <T> gibt an, das es sich bei dieser Klasse um eine generische handelt, anstelle des T kommt beim Gebrauch der Klasse der tatsächlich benötigte Typ zum Einsatz.

Wenn Sie also beispielsweise eine List aus String benötigen, dann können Sie diese wie folgt instanzieren:

List<string> list = new List<string>();

Dabei sind Templates nicht auf Reference-Types beschränkt, ebenso eignen sich Value-Types als Typ-Parameter. Eine Liste aus Integern sieht also wie folgt aus:

List<int> list = new List<int>();

Der Typ-Parameter typisiert die Liste-Collection vollständig. Wenn Sie die Liste indizieren, dann erhalten Sie als Ergebnis auch ein Objekt vom Typ des Typ-Parameters – und nicht wie bei den bisherigen .Net-Collections ein "object". Der folgende Ausdruck (mit einer List<string>) ist also auch ganz ohne Typecast korrekt:

string var = list[0];

Eigene Klassen verwenden

Die Generics sind aber nicht auf die vorgefertigten generischen Collections beschränkt, sondern eignen sich auch für den Einsatz in Ihren eigenen Klassen. Dabei wird der Typ-Parameter hinter dem Namen der Klasse in spitzen Klammern eingeführt. Der Name des Typ-Parameters ist gleichgültig, gängigerweise wird aber der Buchstabe T verwenden.

Innerhalb der Klassendefinition wird der Parameter dann wie ein echter Typ verwendet. Ein einfacher generischer Container für Objekte vom beliebigen Typ sähe also wie folgt aus. Im Beispiel hat der Container einen Konstruktor der ein Objekt vom "richtigen" Typ übergeben bekommt und eine Methode mit der das Objekt erfragt werden kann.

public class Test<T>

{

private T t;

public Test(T t)

{

this.t = t;

}

public T Get()

{

return t;

}

}

Zusammen mit einem String könnte der Container dann wie folgt benutzt werden.

Test<string> t = new Test<string>("hallo generics");

Flexible Typ-Parameter und Constraints

Generics sind nicht auf einen Typ-Parameter beschränkt. Wenn etwa der Beispiel-Container eine Funktion erfüllen würde, die ein Objekt eines anderen Typs als des Typ-Parameters erzeugt, dann bräuchten Sie zwei separate Typ-Parameter: Einen für den Eingangs-Typen und einen für die Angabe des Typs des Produktes.

pulic class Test<T, T_RESULT>

{

}

In diesem Fall erhalten Sie zwei Typ-Parameter, die Sie innerhalb der Klassendefinition verwenden können.

Ohne weitere Informationen ist diese Verwendung von Typ-Parametern zwar praktisch, aber viel mehr Möglichkeiten als die Konstruktion von weiteren Collections stehen Ihnen damit noch nicht zur Verfügung. Eine Einschränkung ist beispielsweise, dass Sie keine Methoden des Typs aufrufen können: Der Compiler weiß einfach nicht, welche Methoden der Typ zur Verfügung stellt.

Dieses Problem beheben die neuen "Constraints". Dabei handelt es sich um Einschränkungen, die Bedingungen für den Typen festlegen. Das erreichen Sie beispielsweise, indem Sie ein zu implementierendes Interface vorschreiben.

Angenommen, Sie haben ein Interface das die Methode Calc() vorschreibt:

public interface ICalculate

{

double Calc();

}

Dieses Interface können Sie im Anschluss als Constraint für Ihren generischen Typen verwenden.

public class Test<T> where T: ICalculate

{

Beispiele für Implementierungen

Dank dem Constraints ist künftig bekannt, dass alle als Typ-Parameter übergebenen Typen das Interface ICalculate implementieren müssen. Sie können also auch alle Methoden des Interfaces verwenden, zum Beispiel um im folgenden Beispiel eine Methode ProduceResult() zum implementieren:

public class Test<T> where T: ICalculate

{

private T t;

public Test(T t)

{

this.t = t;

}

public double ProduceResult()

{

return t.Calc();

}

}

Typ-Parameter können übrigens nicht nur auf Klassen-Ebene angewendet werden. Sie können auch bei nicht generischen Klassen Methoden mit Typ-Parameter definieren. Eine solche Methode die im Beispiel eine typisierte "null" liefert, sieht dann wie folgt aus:

public class Test

{

public T ProduceResult<T>() where T: class

{

return null as T;

}

}

Anonyme Methoden

Anonyme Methoden geben dem Entwickler die Möglichkeit, Inline-Codeblöcke an Stelle von Delegates zu verwenden. Dabei haben diese Codeblöcke auch direkten Zugriff auf die den Codeblock umgebenden lokalen Variablen.

Delegates werden im .Net Framework nahezu überall verwendet. Das unangenehme daran ist, dass Sie oft eine Methode oder ein Klasse implementieren müssen, nur um den Delegate einsetzen zu können. Anonyme Methoden lösen dieses Problem elegant. Ein klassischer Aufruf eines Delegates könnte wie folgt aussehen:

public class Test

{

delegate void EinDelegate();

public void Invoke()

{

EinDelegate d = new EinDelegate(EineMethode);

d();

}

void EineMethode()

{

// do something ...

}

}

Eleganter wird der Aufruf, wenn Sie eine anonyme Methode verwenden:

public class Test

{

delegate void EinDelegate();

public void Invoke()

{

EinDelegate d = delegate() { MessageBox.Show("Test"); };

d();

}

}

Der Code des Delegate kann dabei auch auf die Variablen seiner Umgebung zugreifen. Im Beispiel könnten Sie also sowohl auf lokale Variable der Invoke()-Methode als auch auf die privaten Member von "Test" zugreifen.

Parameter einsetzen

Hat der Delegate Parameter, kann die anonymen Methoden auch auf diese zugreifen, wenn Sie das Schlüsselwort "delegate" anhängen. Hier ein Beispiel mit einem Integer-Parameter:

public class Test

{

delegate void EinDelegate(int x);

public void Invoke()

{

EinDelegate d = delegate( int x) { MessageBox.Show("Test" + x.ToString()); };

d(5);

}

}

Schließlich gibt es noch eine spezielle Variante von anonymen Methoden. Dabei lassen Sie die runden Klammern nach dem "delegate" Schlüsselwort einfach weg. Dadurch definieren Sie eine Methode die jedem beliebigen Delegate zugewiesen werden kann - ganz gleichgültig, wie dessen Signatur aussieht.

Der Compiler erzeugt dann anonyme Parameter, die zur Signatur des Delegate passen. Diese müssen Sie allerdings beim Aufruf angeben.

Neuheit Delegate Inference

Eine weitere Änderung ist die "Delegate Inference". Dabei können Sie auf das Instanzieren des Delegate-Objektes vollständig verzichten, stattdessen schreiben Sie einfach nur den Namen der zu verwendenden Methode hin.

Statt der klassischen Schreibweise

public class Test

{

delegate void EinDelegate();

public void Invoke()

{

EinDelegate d = new EinDelegate(EineMethode);

d();

}

void EineMethode()

{

// do something ...

}

}

gilt also genauso die vereinfachte Neue:

public class Test

{

delegate void EinDelegate();

public void Invoke()

{

EinDelegate d = EineMethode;

d();

}

void EineMethode()

{

// do something ...

}

}

Iteratoren

Im alten C# konnten Sie über eine beliebige Collection mit dem foreach() Statement iterieren. Vorraussetzung war, dass diese Collection eine GetEnumerator() Methode implementierte, die ein IEnumerator Interface lieferte. Der Iterator wurde dabei meistens als nested Class der Collection implementiert. Das zentrale Problem mit diesem Ansatz ist, dass IEnumerator.Current ein "object" liefert. Enthält die Collection nämlich Value-Types, führt das beim iterieren zum dauernden boxen- und unboxen der Elemente aus der Collection. Selbst wenn die Collection Reference-Types enthält gibt es ein Performance-Problem durch den benötigten Downcast.

Im neuen Framework gibt es daher die IEnumerable<ItemType> und IEnumerator<ItemType> Interfaces. C# 2.0 nutzt diese beiden Zusammen mit dem neuen Yield-Statement. Das neue Statement ist ungeheuer praktisch, lässt es den Compiler doch die Implementierung von IEnumerator automatisch erzeugen. Alles was Sie noch tun müssen ist dem Compiler mitzuteilen, welches Element pro Iteration geliefert werden soll. Eine komplette Implementierung sieht dann in C#2 wie folgt aus:

public class NameCollection : IEnumerable<string>

{

string[] names = { "Peter", "Paul", "Mary" };

public IEnumerator<string> GetEnumerator()

{

for (int i = 0; i < names.Length; i++)

yield return names[i];

}

}

Partial Types

In C# 1.x mussten der vollständige Code einer Klasse in einer einzelnen Datei enthalten sein. C# 2 erlaubt es dank Partial Types jetzt, die notwendigen Anweisungen über mehrere Dateien zu verteilen. Das funktioniert mit Klassen, Structs und Interfaces, aber nicht mit Enums.

Um eine Klasse aufzuteilen, verwenden Sie das neue Schlüsselwort "partial". In der ersten Datei könnte also zum Beispiel folgender Code stehen:

public partial class Meine

{

public void Method1() {}

}

In einer weiteren Datei kann es dann zusätzlich noch folgenden Code geben:

public partial class Meine

{

public void Method2() {}

}

Der Compiler baut dann daraus eine gemeinsame Klasse "Meine" zusammen, die beide Methoden enthält. Das Visual Studio nutzt dieses Feature beim Forms-Designer, der aktuell den automatisch generierten Code in einer anderen Datei ablegt, als den Code, den Sie programmiert haben. (mja)