Künftige Features der C#-Programmiersprache

Veröffentlicht: 13. Mai 2003 | Aktualisiert: 22. Jun 2004

Von Prashant Sridharan

Inhalt

Einführung
Generik
Iteratoren
Anonyme Methoden
Teiltypen
Standardkonformität
Verfügbarkeit
Weitere Informationen

Einführung

C# ist eine moderne und innovative Programmiersprache, die eine sorgfältige Integration von Features aus den meisten gängigen Industrie- und Forschungssprachen umfasst. Im Sinne der Entwurfsphilosophie von C# hat Microsoft verschiedene potenzielle neue Features in C# eingeführt, die die Produktivität von Entwicklern in Bezug auf Sprachkonstrukte erhöhen.

 

Microsoft C#

Seit ihrer Einführung im Februar 2001 wird die C#-Programmiersprache von Entwicklern zunehmend zum Erstellen von Software eingesetzt. Selbst bei Microsoft wurden verschiedene kommerzielle Anwendungen mit C# erstellt, einschließlich .NET Framework, MSN Web-Eigenschaften und das Tablet PC-SDK. C# hat sich somit als Sprache erwiesen, die für die Konstruktion von hochwertiger kommerzieller Software geeignet ist.

Viele der Features in der C#-Sprache wurden unter Berücksichtigung der folgenden vier Entwurfsziele erstellt:

  • Einheitliches Typensystem und Vereinfachung der Art und Weise, in der Wert- und Verweistypen von der Sprache verwendet werden.

  • Komponentenbasierter Entwurf durch Verwendung von Features wie XML-Kommentare, Attribute, Eigenschaften, Ereignisse und Delegate.

  • Schaffung eines praktischen Entwicklungsspielraums durch die einmaligen Funktionen der C#-Sprache, einschließlich sicherer Zeigermanipulation, Überlaufprüfung usw.

  • Pragmatische Sprachkonstrukte, wie die Anweisungen foreach und using, die die Entwicklerproduktivität erhöhen.

Microsoft plant, der "Visual Studio for Yukon"-Version der C#-Sprache eine elegante und ausdrucksvolle Syntax zugrunde zu legen. Dies soll durch Integration einer Reihe von Features aus einem breiten Spektrum von Forschungs- und Industriesprachen geschehen. Zu diesen Sprachfeatures gehören Generik, Iteratoren, anonyme Methoden und Teiltypen.

 

Potenzielle künftige Features

In der Tat basieren künftige Innovationen in C# auf einem einheitlichen Typensystem, einer komponentenbasierten Entwicklung, Entwicklungsspielraum und pragmatischen Sprachkonstrukten. Im Folgenden werden vier wichtige neue Features beschrieben, die Microsoft voraussichtlich in der nächsten Hauptversion der C#-Sprache bereitstellen wird. Die Entwurfsphase ist noch nicht abgeschlossen, und die Microsoft Corporation würde Kommentare aus der Entwicklercommunity zu diesen Features begrüßen.

 

Generik

Da die Projekte heutzutage immer komplexer werden, benötigen immer mehr Programmierer eine Möglichkeit, vorhandene komponentenbasierte Software besser wieder verwenden und anpassen zu können. Sie setzen gewöhnlich ein Feature namens Generik ein, um Code in anderen Sprachen in solch großem Umfang wieder verwenden zu können. C# soll eine typensichere Hochleistungsversion von Generik umfassen, die in Bezug auf die Syntax nur geringfügig von den in C++ enthaltenen Vorlagen und der für die Java-Sprache vorgeschlagenen Generik abweicht, bei der Implementierung jedoch große Unterschiede aufweist.

 

Erstellen von generischen Klassen heute

Mit dem heutigen C# können Programmierer eine beschränkte Version von echten generischen Typen erstellen, indem sie Daten in Instanzen des Basisobjekttyps speichern. Da jedes Objekt in C# vom Basisobjekttyp erbt und das einheitliche .NET-Typensystem Boxing- und Unboxingfeatures enthält, können Programmierer sowohl Verweis- als auch Werttypen in einer Variable vom Typ Objekt speichern. Umwandlungen zwischen Verweistypen und Werttypen und dem Basisobjekttyp gehen jedoch zu Lasten der Leistung.

Der folgende Code veranschaulicht die Erstellung eines einfachen Stack-Typs mit zwei Aktionen: Push und Pop. Die Stack-Klasse speichert ihre Daten in einem Array von Objekttypen, und die Methoden Push und Pop verwenden den Basisobjekttyp, um die Daten zu akzeptieren bzw. zurückzugeben.

public class Stack 
{ 
   private object[] items = new object[100]; 
   public void Push(object data) 
   { 
   ... 
   } 
   public object Pop() 
   { 
   ... 
   } 
}

Dann kann dem Stapel durch einen Push ein benutzerdefinierter Typ (z.B. ein Customer-Typ) hinzugefügt werden. Wenn das Programm die Daten jedoch abrufen muss, muss es das Ergebnis der Pop-Methode, einen Basisobjekttyp, explizit in einen Customer-Typ umwandeln.

Stack s = new Stack(); 
s.Push(new Customer()); 
Customer c = (Customer) s.Pop();

Wenn ein Werttyp (z.B. eine Ganzzahl) an die Push-Methode übergeben wird, wandelt die Laufzeit diesen automatisch in einen Verweistyp um - ein Vorgang, der als Boxing bezeichnet wird - und speichert ihn anschließend in der internen Datenstruktur. Gleichermaßen muss das Programm den über die Pop-Methode abgerufenen Objekttyp explizit in einen Werttyp umwandeln, wenn das Programm einen Werttyp (z.B. eine Ganzzahl) aus dem Stapel abrufen soll. Dieser Vorgang wird Unboxing genannt.

Stack s = new Stack(); 
s.Push(3); 
int i = (int) s.Pop();

Das Boxing und Unboxing von Wert- und Verweistypen kann äußerst beschwerlich sein.

Außerdem ist es in der aktuellen Implementierung nicht möglich, die Art der im Stapel platzierten Daten zu erzwingen. In der Tat könnte ein Stapel erstellt werden, dem dann per Push ein Customer-Typ hinzugefügt wird. Später könnte derselbe Stapel verwendet werden und versuchen, Daten per Pop zu entfernen und als anderen Typ festzulegen. Dies wird im folgenden Beispiel gezeigt:

Stack s = new Stack(); 
s.Push(new Customer()); 
Employee e = (Employee) s.Pop();

Das oben stehende Codebeispiel zeigt eine unangemessene Verwendung der Einzeltyp-Stack-Klasse, die für die Implementierung erwünscht ist und eigentlich ein Fehler sein sollte. Im Grunde genommen handelt es sich dabei jedoch um legalen Code, der dem Compiler keine Probleme bereiten würde. Zur Laufzeit würde das Programm jedoch aufgrund eines ungültigen Umwandlungsvorgangs fehlschlagen.

 

Erstellen und Verwenden von Generik

Die Generik in C# bietet eine Möglichkeit zum Erstellen von Hochleistungsdatenstrukturen, die vom Compiler basierend auf den von ihnen verwendeten Typen spezialisiert werden. Diese so genannten parametrisierten Typen werden so erstellt, dass ihre internen Algorithmen gleich bleiben, die Typen ihrer internen Daten jedoch je nach Präferenz des Endbenutzers abweichen können.

Zur Minimierung der Lernkurve für Entwickler wird die Generik in C# fast genauso deklariert wie in C++. Programmierer können Klassen und Strukturen auf gewohnte Weise erstellen und Typparameter in spitzen Klammern (< und >) angeben. Wenn die Klasse verwendet wird, muss jeder Parameter durch einen tatsächlichen Typ ersetzt werden, den der Benutzer der Klasse bereitstellt.

Im folgenden Beispiel wird eine Stack-Klasse erstellt, wobei nach der Klassendeklaration ein Typparameter namens ItemType festgelegt und in spitzen Klammern deklariert wird. Statt Umwandlungen in und aus dem Basisobjekttyp zu erzwingen, akzeptieren Instanzen der generischen Stack-Klasse den Typ, für den sie erstellt werden, und speichern Daten dieses Typs im System. Der Typparameter ItemType fungiert als Proxy, bis er bei der Instanziierung angegeben und als Typ für das Array der internen Elemente verwendet wird - der Typ für den Parameter zur Push-Methode sowie der Rückgabetyp für die Pop-Methode:

public class Stack<ItemType> 
{ 
   private ItemType[] items; 
   public void Push(ItemType data) 
   { 
   ... 
   } 
   public ItemType Pop() 
   { 
   ... 
   } 
}

Wenn das Programm wie im folgenden Beispiel die Stack-Klasse verwendet, können Sie den tatsächlichen Typ angeben, der von der generischen Klasse verwendet werden soll. In diesem Fall weisen Sie die Stack-Klasse an, einen primitiven Ganzzahltyp zu verwenden, indem Sie ihn unter Verwendung von spitzen Klammern in der Instanziierungsanweisung angeben:

Stack<int> stack = new Stack<int>(); 
stack.Push(3); 
int x = stack.Pop();

Dadurch erstellt das Programm eine neue Instanz der Stack-Klasse, für die jeder ItemType-Typparameter durch den bereitgestellten Ganzzahlparameter ersetzt wird. Wenn das Programm also die neue Instanz der Stack-Klasse mit einem Ganzzahlparameter erstellt, ist der systemeigene Speicher des Elementarrays in der Stack-Klasse nun eine Ganzzahl statt eines Objekts. Außerdem hat das Programm die Boxingnachteile beseitigt, die mit dem Push von Ganzwerten auf den Stapel verbunden sind. Darüber hinaus müssen Sie Elemente, die vom Programm per Pop aus dem Stapel entfernt werden, nicht mehr explizit in den entsprechenden Typ umwandeln, da diese spezielle Instanz der Stack-Klasse automatisch eine Ganzzahl in ihrer Datenstruktur speichert.

Wenn das Programm keine Ganzzahlen, sondern andere Elemente in einer Stack-Klasse speichern soll, müssen Sie eine neue Instanz der Stack-Klasse erstellen und dabei den neuen Typ als Parameter angeben. Nehmen wir an, Sie haben einen einfachen Customer-Typ und möchten, dass das Programm ihn unter Verwendung eines Stack-Objekts speichert. Instanziieren Sie dazu einfach die Stack-Klassen mit dem Customer-Objekt als Typparameter. Dann können Sie den Programmcode ganz einfach wieder verwenden:

Stack<Customer> stack = new Stack<Customer>(); 
stack.Push(new Customer()); 
Customer c = stack.Pop();

Nachdem das Programm eine Stack-Klasse mit einem Customer-Typ als Parameter erstellt hat, kann es nun natürlich nur noch Customer-Typen im Stapel speichern. Die Generik in C# ist in der Tat stark typisiert, Ganzzahlen können nicht mehr falsch im Stapel gespeichert werden, wie im folgenden Beispiel gezeigt:

Stack<Customer> stack = new Stack<Customer>(); 
stack.Push(new Customer()); 
stack.Push(3)  // compile-time error 
Customer c = stack.Pop();   // no cast required.

 

Vorteile der Generik

Mit Hilfe von Generik brauchen Programmier Code nur einmal zu schreiben, zu testen und bereitzustellen und können ihn dann für eine Reihe unterschiedlicher Datentypen wieder verwenden. Dies gilt zwar auch für das erste Stapelbeispiel, doch kann Ihr Programm den Code im zweiten Stapelbeispiel ohne größere Leistungsverluste für Anwendungen wieder verwenden. Für Werttypen ist das erste Stapelbeispiel mit großen Leistungsverlusten verbunden, während diese Verluste im zweiten Stapelbeispiel vollständig beseitigt werden, da Boxing und Umwandlung entfallen.

Außerdem wird die Generik zur Kompilierzeit überprüft. Wenn das Programm eine generische Klasse mit einem bereitgestellten Typparameter instanziiert, kann dieser Typparameter nur dem Typ angehören, den das Programm in der Klassendefinition angegeben hat. Wenn das Programm z.B. einen Stapel von Customer-Objekten erstellt hat, konnte es keinen Push mehr durchführen, um dem Stapel eine Ganzzahl hinzuzufügen. Durch Erzwingung eines solchen Verhaltens können Sie zuverlässigeren Code erstellen.

Weiterhin reduziert die C#-Implementierung von Generik im Gegensatz zu anderen stark typisierten Implementierungen den Codeumfang. Durch das Erstellen von typisierten Auflistungen mit Generik brauchen Sie keine spezifischen Versionen der einzelnen Klassen mehr zu erstellen und können somit die damit verbundenen Leistungsvorteile beibehalten. Ihr Programm kann z.B. eine parametrisierte Stack-Klasse erstellen und vermeiden, eine IntegerStack-Klasse zum Speichern von Ganzzahlen, eine StringStack-Klasse zum Speichern von Zeichenfolgen oder eine CustomerStack-Klasse zum Speichern von Customer-Typen erstellen zu müssen.

Dadurch wird der Code natürlich besser lesbar. Durch Erstellung einer Stack-Klasse kann das Programm sämtliches, mit einem Stapel verbundenes Verhalten in einer praktischen Klasse kapseln. Wenn Sie dann einen Stapel von Customer-Typen erstellen, ist nach wie vor ersichtlich, dass das Programm eine Stapeldatenstruktur verwendet, auch wenn Customer-Typen in ihr gespeichert sind.

 

Mehrere Typparameter

Generische Typen können eine Reihe von Parametertypen verwenden. Im vorherigen Stapelbeispiel wurde nur ein Typ verwendet. Nehmen wir an, Sie erstellen eine einfache Dictionary-Klasse, die Werte zusammen mit Schlüsseln speichert. Ihr Programm könnte eine generische Version einer Dictionary-Klasse definieren, indem es zwei durch Kommas getrennte Parameter in den spitzen Klammern der Klassendefinition deklariert:

public class Dictionary<KeyType, ValType> 
{ 
   public void Add(KeyType key, ValType val) 
   { 
   ... 
   } 
   public ValType this[KeyType key] 
   { 
   ... 
   } 
}

Wenn Sie die Dictionary-Klasse verwenden, müssen Sie mehrere, ebenfalls durch Kommas getrennte Parameter in den spitzen Klammern der Instanziierungsanweisung bereitstellen und die richtigen Typen für die Parameter der Add-Funktion und des Indexers angeben:

Dictionary<int, Customer> dict = new Dictionary<int, Customer>(); 
dict.Add(3, new Customer()); 
Customer c = dict.Get[3];

 

Einschränkungen

Im Allgemeinen tut Ihr Programm mehr, als nur Daten basierend auf einem bestimmten Typparameter zu speichern. Häufig soll das Programm Members des Typparameters verwenden, um Anweisungen im generischen Typ des Programms auszuführen.

 

Gründe für die Notwendigkeit von Einschränkungen

Nehmen wir z.B. an, Sie möchten in der Add-Methode der Dictionary-Klasse mit Hilfe der CompareTo-Methode des bereitgestellten Schlüssels Elemente vergleichen:

public class Dictionary<KeyType, ValType> 
{ 
   public void Add(KeyType key, ValType val) 
   { 
   ... 
   switch(key.CompareTo(x)) 
   { 
   } 
   ... 
   } 
}

Wie erwartet, ist der Typparameter KeyType zur Kompilierzeit leider generisch. In seiner jetzigen Form nimmt der Compiler an, dass der Schlüsselinstanz des Typparameters KeyType nur die Vorgänge zur Verfügung stehen, die für einen Basisobjekttyp wie ToString verfügbar sind. Folglich zeigt der Compiler einen Kompilierfehler an, da die CompareTo-Methode nicht definiert ist. Das Programm kann die Schlüsselvariable jedoch in ein Objekt umwandeln, das eine CompareTo-Methode enthält, z.B. eine IComparable-Schnittstelle. Im folgenden Beispiel wandelt das Programm die Instanz des als Schlüssel bezeichneten Parametertyps KeyType explizit in eine IComparable-Schnittstelle um und kann somit die Kompilierung durchführen:

public class Dictionary<KeyType, ValType> 
{ 
   public void Add(KeyType key, ValType val) 
   { 
   ... 
   switch(((IComparable) key).CompareTo(x)) 
   { 
   } 
   ... 
   } 
}

Wenn Sie nun jedoch eine Dictionary-Klasse instanziieren und einen Typparameter angeben, der die IComparable-Schnittstelle nicht implementiert, tritt im Programm ein Laufzeitfehler auf, genauer gesagt ein InvalidCastException-Fehler.

 

Deklarieren von Einschränkungen

In C# kann Ihr Programm eine optionale Liste von Einschränkungen für die einzelnen, in Ihrer generischen Klasse deklarierten Typparameter bereitstellen. Eine Einschränkung weist auf eine Anforderung hin, die ein Typ erfüllen muss, um einen generischen Typ zu konstruieren. Einschränkungen werden unter Verwendung des where-Schlüsselworts deklariert, gefolgt von einem "Parameter/Anforderung"-Paar, wobei es sich bei dem "Parameter" um einen der im generischen Typ definierten Parameter und bei der "Anforderung" um eine Klasse oder Schnittstelle handeln muss.

Der Notwendigkeit, die CompareTo-Methode in der Dictionary-Klasse zu verwenden, kann das Programm entsprechen, indem es dem KeyType-Typparameter eine Einschränkung auferlegt, die es erforderlich macht, dass jeder Typ, der an den ersten Parameter der Dictionary-Klasse übergeben wird, die IComparable-Schnittstelle implementiert. Dies wird im folgenden Beispiel gezeigt:

public class Dictionary<KeyType, ValType> where KeyType : IComparable 
{ 
   public void Add(KeyType key, ValType val) 
   { 
   ... 
   switch(key.CompareTo(x)) 
   { 
   } 
   ... 
   } 
}

Nun wird Ihr Code beim Kompilieren überprüft, um sicherzustellen, dass das Programm jedes Mal, wenn es die Dictionary-Klasse verwendet, als ersten Parameter einen Typ übergibt, der die IComparable-Schnittstelle implementiert. Außerdem muss das Programm die Variable nicht mehr explizit in eine IComparable-Schnittstelle umwandeln, bevor es die CompareTo-Methode aufruft.

 

Mehrere Einschränkungen

Ihr Programm kann für jeden gegebenen Typparameter eine beliebige Anzahl von Schnittstellen als Einschränkungen festlegen, jedoch grundsätzlich nur eine Klasse. Jede neue Einschränkung wird als weiteres Parameter/Anforderung-Paar deklariert, wobei die einzelnen Einschränkungen für einen gegebenen generischen Typ durch Kommas getrennt werden. Im folgenden Beispiel enthält die Dictionary-Klasse zwei Parametertypen: KeyType und ValType. Der KeyType-Typparameter ist mit zwei Schnittstelleneinschränkungen versehen, während der ValType-Typparameter eine Klasseneinschränkung hat:

public class Dictionary<KeyType, ValType> where 
KeyType : IComparable, 
KeyType : IEnumerable, 
ValType : Customer 
{ 
   public void Add(KeyType key, ValType val) 
   { 
   ... 
   switch(key.CompareTo(x)) 
   { 
   } 
   ... 
   } 
}

 

Generik in der Laufzeit

Beim Kompilieren unterscheiden sich generische Klassen im Grunde genommen kaum von regulären Klassen. In der Tat ergibt die Kompilierung nichts anderes als Metadaten und IL (Intermediate Language). Die IL ist natürlich parametrisiert, um irgendwo im Code einen vom Benutzer bereitgestellten Typ zu akzeptieren. Wie die IL für einen generischen Typ verwendet wird, hängt davon ab, ob es sich bei dem bereitgestellten Typparameter um einen Wert- oder Verweistyp handelt.

Wenn ein generischer Typ erstmals mit einem Werttyp als Parameter konstruiert wird, erstellt die Laufzeit einen speziellen generischen Typ mit den bereitgestellten Parametern, die an den entsprechenden Stellen in der IL ersetzt wurden. Spezielle generische Typen werden einmal für jeden eindeutigen, als Parameter verwendeten Werttyp erstellt.

Nehmen wir z.B. an, Ihr Programmcode deklariert einen aus Ganzzahlen konstruierten Stapel:

Stack<int> stack;

An diesem Punkt generiert die Laufzeit eine spezielle Version der Stack-Klasse, wobei ihr Parameter entsprechend durch die Ganzzahl ersetzt wird. Wenn der Programmcode nun einen Stapel von Ganzzahlen verwendet, wird die generierte spezielle Stack-Klasse von der Laufzeit wieder verwendet. Im folgenden Beispiel werden zwei Instanzen eines Stapels von Ganzzahlen erstellt, die beide den Code verwenden, der von der Laufzeit bereits für einen Stapel von Ganzzahlen generiert wurde:

Stack<int> stackOne = new Stack<int>(); 
Stack<int> stackTwo = new Stack<int>();

Wenn jedoch an einer anderen Stelle in Ihrem Programmcode eine weitere Stack-Klasse mit einem anderen Werttyp erstellt wird (z.B. mit einer Long- oder benutzerdefinierten Struktur als Parameter), generiert die Laufzeit eine weitere Version des generischen Typs, wobei diesmal ein Long-Parameter an den entsprechenden Stellen in der IL ersetzt wird. Das Erstellen von speziellen Klassen für mit Werttypen konstruierte Generik hat den Vorteil einer besseren Leistung. Schließlich sind keine Umwandlungen mehr nötig, da jede spezielle generische Klasse "automatisch" den Werttyp enthält.

Bei Verweistypen funktioniert Generik ein wenig anders. Wenn erstmals ein generischer Typ mit einem beliebigen Verweistyp konstruiert wird, erstellt die Laufzeit einen speziellen generischen Typ mit Objektverweisen, die die Parameter an den entsprechenden Stellen in der IL ersetzen. Dann wird jedes Mal, wenn ein konstruierter Typ mit einem Verweistyp als Parameter instanziiert wird, die zuvor erstellte spezielle Version des generischen Typs von der Laufzeit wieder verwendet - unabhängig vom Typ des Parameters.

Nehmen wir z.B. an, Sie verfügen über zwei Verweistypen, eine Customer-Klasse und eine Order-Klasse, und haben außerdem einen Stapel von Customer-Typen erstellt:

Stack<Customer> customers;

An diesem Punkt generiert die Laufzeit eine spezielle Version der Stack-Klasse, die statt Daten später einzufügende Objektverweise speichert. Nehmen wir an, dass die nächste Codezeile einen Stapel eines anderen Verweistyps namens Order erstellt:

Stack<Order> orders = new Stack<Order>();

Anders als bei Werttypen wird für den Order-Typ keine weitere spezielle Version der Stack-Klasse erstellt. Stattdessen wird eine Instanz der speziellen Version der Stack-Klasse erstellt, und die Orders-Variable wird als Verweis darauf festgelegt. Für jeden Objektverweis, durch den der Typparameter ersetzt wurde, wird ein Speicherbereich von der Größe eines Order-Typs zugeordnet, und der Zeiger wird als Verweis auf diesen Speicherort festgelegt. Nehmen wir an, Sie wären auf eine Codezeile gestoßen, die einen Stapel eines Customer-Typs erstellt:

customers = new Stack<Customer>();

Wie bei der vorherigen Verwendung der mit dem Order-Typ erstellten Stack-Klasse wird eine weitere Instanz der speziellen Stack-Klasse erstellt, und die darin enthaltenen Zeiger werden als Verweise auf einen Speicherbereich von der Größe eines Customer-Typs festgelegt. Da die Anzahl der Verweistypen von Programm zu Programm stark variieren kann, reduziert die C#-Implementierung der Generik den Codeumfang erheblich, indem sie die Anzahl der speziellen Klassen, die vom Compiler für generische Klassen von Verweistypen erstellt werden, auf lediglich eine reduziert.

Außerdem kann eine generische C#-Klasse, die mit einem Typparameter instanziiert wird - unabhängig davon, ob es sich um einen Wert- oder Verweistyp handelt - unter Verwendung von Reflektion abgefragt werden. Sowohl ihr tatsächlicher Typ als auch ihr Typparameter können zur Laufzeit ermittelt werden.

 

Unterschiede zwischen C#-Generik und anderen Implementierungen

C++-Vorlagen unterscheiden sich stark von der C#-Generik. Während die C#-Generik in IL kompiliert wird, wobei zur Laufzeit für jeden Werttyp und lediglich einmal pro Verweistyp eine intelligente Spezialisierung erfolgt, handelt es sich bei C++-Vorlagen im Wesentlichen um Codeerweiterungsmakros, die einen speziellen Typ für die einzelnen, für die jeweilige Vorlage bereitgestellten Typparameter generieren. Wenn der C++-Compiler also auf eine Vorlage trifft, z.B. einen Stapel von Ganzzahlen, erweitert er den Vorlagencode auf eine Stack-Klasse, die Ganzzahlen intern als systemeigenen Typ enthält. Unabhängig davon, ob der Typparameter ein Wert- oder Verweistyp ist (es sei denn, der Linker wurde eigens dazu entworfen, den Codeumfang zu reduzieren), erstellt der C++-Compiler jedes Mal eine spezielle Klasse, wodurch sich der Codeumfang im Vergleich zur C#-Generik bedeutend erhöht.

Außerdem können C++-Vorlagen keine Einschränkungen definieren. C++-Vorlagen können Einschränkungen nur implizit definieren, indem sie einfach ein Member verwenden, das zum Typparameter gehören kann oder auch nicht. Wenn das Member in dem Typparameter vorhanden ist, der irgendwann an die generische Klasse übergeben wird, funktioniert das Programm ordnungsgemäß. Wenn das Member jedoch nicht in diesem Typparameter vorhanden ist, schlägt das Programm fehl, und es wird wahrscheinlich eine kryptische Fehlermeldung zurückgegeben. Da C#-Generik Einschränkungen deklarieren kann und stark typisiert ist, treten diese potenziellen Fehler nicht auf.

Sun Microsystems® hat unterdessen vorgeschlagen, der nächsten Version der Java-Sprache, die den Codenamen "Tiger" trägt, Generik hinzuzufügen. Sun hat sich für eine Implementierung entschieden, die keine Änderung der Java Virtual Machine erforderlich macht. Somit steht Sun vor der Aufgabe, Generik auf einem nicht geänderten virtuellen Computer zu implementieren.

Der Implementierungsvorschlag von Java verwendet eine ähnliche Syntax wie Vorlagen in C++ und Generik in C#, einschließlich Typparametern und Einschränkungen. Da die Java Virtual Machine Wertetypen jedoch anders behandelt als Verweistypen, kann sie keine Generik für Werttypen unterstützen. Aus diesem Grund trägt Generik in Java nicht zu einer effizienteren Ausführung bei. In der Tat fügt der Java-Compiler immer dann, wenn er Daten zurückgeben muss, eine automatische Umwandlung der speziellen Einschränkung (sofern eine deklariert ist) oder des Basisobjekttyps (falls keine Einschränkung deklariert ist) ein. Weiterhin generiert der Java-Compiler einen speziellen Typ zur Kompilierzeit, mit dessen Hilfe er dann beliebige konstruierte Typen instanziiert. Da die Java Virtual Machine Generik nicht von sich aus unterstützt, gibt es letztendlich keine Möglichkeit, den Typparameter für eine Instanz eines generischen Typs zur Laufzeit zu ermitteln, und weitere Verwendungsmöglichkeiten der Reflektion werden erheblich eingeschränkt.

 

Generikunterstützung in anderen Sprachen

Microsoft plant, die Verwendung und Erstellung von generischen Typen in Visual J#TM, Visual C++ und Visual Basic zu unterstützen. Einige Sprachen implementieren dieses Feature zwar unter Umständen früher als andere, doch sollen alle drei anderen Sprachen von Microsoft Unterstützung für Generik umfassen. Das C#-Team schafft unterdessen das Grundgerüst für mehrsprachige Unterstützung, indem es entsprechende Funktionen in die zugrunde liegende Laufzeit für Generik integriert. Microsoft arbeitet eng mit Drittsprachenpartnern zusammen, um sicherzustellen, dass die Erstellung und Verwendung von Generik die gesamte Bandbreite der .NET-basierten Sprachen umfasst.

 

Iteratoren

Ein Iterator ist ein Sprachkonstrukt, das auf ähnlichen Features basiert, wie sie in den Forschungssprachen CLU, Sather und Icon zu finden sind. Mit einfachen Worten: Iteratoren erleichtern Typen die Deklaration der Art und Weise, in der die foreach-Anweisung die Elemente iterativ durchläuft.

Gründe für die Verwendung von Iteratoren
Heutzutage müssen Klassen das "Enumeratormuster" verwenden, wenn sie Iteration durch Verwendung des foreach-Schleifenkonstrukts unterstützen sollen. Das foreach-Schleifenkonstrukt auf der linken Seite wird z.B. vom Compiler bis in das while-Schleifenkonstrukt auf der rechten Seite erweitert.

List list = ...; 
foreach(object obj in list) 
{ 
DoSomething(obj); 
}
Enumerator e = list.GetEnumerator(); 
while(e.MoveNext()) 
{    
   object obj = e.Current;    
   DoSomething(obj);

Beachten Sie, dass die List-Datenstruktur - die zu durchlaufende Instanz - die GetEnumerator-Funktion unterstützen muss, damit die foreach-Schleife funktionieren kann. Nachdem die List-Datenstruktur erstellt wurde, muss die GetEnumerator-Funktion implementiert werden, die dann ein ListEnumerator-Objekt zurückgibt:

public class List 
{ 
   internal object[] elements; 
   internal int count; 
   public ListEnumerator GetEnumerator() 
   { 
   return new ListEnumerator(this); 
   } 
}

Das erstellte ListEnumerator-Objekt muss nicht nur die Current-Eigenschaft und die MoveNext-Methode implementieren, sondern auch seinen internen Status beibehalten, damit das Programm nach jeder Schleife zum nächsten Element wechseln kann. Dieser interne Statusmechanismus kann für die List-Datenstruktur zwar einfach sein, für Datenstrukturen, die rekursiv durchlaufen werden müssen (z.B. Binärstrukturen), jedoch recht kompliziert ausfallen.

Da die Implementierung dieses Enumeratormusters einen großen Arbeits- und Codeaufwand für den Entwickler mit sich bringen kann, soll C# ein neues Konstrukt enthalten, das es Klassen erleichtern soll, die Art und Weise festzulegen, in der die foreach-Schleife ihren Inhalt durchläuft.

Definieren von Iteratoren
Da ein Iterator das logische Gegenstück zu einem foreach-Schleifenkonstrukt ist, wird es ähnlich wie eine Funktion definiert, nämlich mit dem foreach-Schlüsselwort, gefolgt von einer öffnenden und einer schließenden Klammer. Im folgenden Beispiel deklariert das Programm einen Iterator für den List-Typ. Der Rückgabetyp des Iterators wird vom Benutzer bestimmt. Da die List-Klasse intern einen Objekttyp speichert, ist der Rückgabetyp des folgenden Iteratorbeispiels jedoch ein Objekt:

public class List 
{ 
   internal object[] elements; 
   internal int count; 
   public object foreach() 
   { 
   } 
}

Beachten Sie, dass das Programm nach der Implementierung des Enumeratormusters einen internen Statusmechanismus beibehalten muss, um verfolgen zu können, an welcher Stelle in der Datenstruktur sich das Programm befindet. Iteratoren besitzen integrierte Statusmechanismen. Durch Verwendung des yield-Schlüsselworts kann das Programm Werte an die foreach-Anweisung zurückgeben, die den Iterator aufgerufen hat. Wenn die foreach-Anweisung das nächste Mal eine Schleife ausführt und den Iterator erneut aufruft, wird die Ausführung dieses Iterators dort aufgenommen, wo die vorherige yield-Anweisung aufgehört hat. Im folgenden Beispiel ergeben sich aus dem Programm drei Zeichenfolgentypen:

public class List 
{ 
   internal object[] elements; 
   internal int count; 
   public string foreach() 
   { 
   yield "microsoft"; 
   yield "corporation"; 
   yield "developer division"; 
   } 
}

Im folgenden Beispiel wird die foreach-Schleife, die diesen Iterator aufruft, drei Mal ausgeführt und empfängt die Zeichenfolgen jedes Mal in der von den vorherigen drei yield-Anweisungen angegebenen Reihenfolge:

List list = new List(); 
foreach(string s in list) 
{ 
Console.WriteLine(s); 
}

Wenn das Programm den Iterator implementieren soll, um die Elemente in der Liste zu durchsuchen, müssen Sie ihn so ändern, dass er die Elemente mit Hilfe einer foreach-Schleife schrittweise durchläuft, damit sich die einzelnen Elemente im Array in jeder Iteration ergeben:

public class List 
{ 
   internal object[] elements; 
   internal int count; 
   public object foreach() 
   { 
   foreach(object o in elements) 
   { 
   yield o; 
   } 
   } 
}

So funktionieren Iteratoren
Iteratoren haben die schwierige Aufgabe, das Enumeratormuster im Auftrag des Programms zu implementieren. Statt die Klassen und den Statusmechanismus erstellen zu müssen, übersetzt der C#-Compiler den im Iterator geschriebenen Code in die entsprechenden Klassen und Code, der das Enumeratormuster verwendet. Auf diese Weise können Iteratoren die Produktivität von Entwicklern bedeutend erhöhen.

 

Anonyme Methoden

Anonyme Methoden stellen ein weiteres praktisches Sprachkonstrukt dar, mit dessen Hilfe Programmierer Codeblöcke erstellen können, die in einem Delegaten gekapselt und zu einem späteren Zeitpunkt ausgeführt werden können. Sie basieren auf einem Sprachkonzept, das als Lambdafunktion bezeichnet wird und den Konzepten in Lisp und Python ähnlich ist.

Erstellen von Delegatencode
Ein Delegat ist ein Objekt, das auf eine Methode verweist. Wenn der Delegat aufgerufen wird, wird auch die Methode, auf die er verweist, aufgerufen. Im folgenden Beispiel wird ein einfaches Formular mit drei Steuerelementen gezeigt: ein Listenfeld (ListBox), ein Textfeld (TextBox) und eine Schaltfläche (Button). Wenn die Schaltfläche initialisiert wird, weist das Programm seinen Click-Delegaten an, auf die AddClick-Methode zu verweisen, die irgendwo im Objekt gespeichert ist. In der AddClick-Methode wird der Wert des Textfelds im Listenfeld gespeichert. Die AddClick-Methode, die dem Click-Delegaten der Schaltflächeninstanz hinzugefügt wurde, wird bei jedem Klick auf die Schaltfläche aufgerufen.

public class MyForm 
{ 
   ListBox listBox; 
   TextBox textBox; 
   Button button; 
   public MyForm() 
   { 
listBox = new ListBox(...); 
textBox = new TextBox(...); 
button = new Button(...); 
button.Click += new EventHandler(AddClick); 
} 
   void AddClick(object sender, EventArgs e) 
   { 
   listBox.Items.Add(textBox.Text); 
   } 
}

Verwenden von anonymen Methoden
Das oben genannte Beispiel ist recht übersichtlich. Es wird eine separate Funktion erstellt und durch den Delegaten mit einem Verweis versehen. Jedes Mal, wenn der Delegat aufgerufen wird, ruft das Programm die Funktion auf. Innerhalb der Funktion wird eine Reihe von ausführbaren Schritten durchgeführt. Durch die Hinzufügung von anonymen Methoden könnte Ihr Programm auf das Erstellen einer gänzlich neuen Methode für die Klasse verzichten und stattdessen direkt über den Delegaten auf die darin enthaltenen ausführbaren Schritte verweisen. Anonyme Methoden werden durch Instanziierung eines Delegaten deklariert, wobei der Instanziierungsanweisung dann ein Paar geschwungene Klammern nachgestellt werden, die einen Ausführungsbereich kennzeichnen, gefolgt von einem Semikolon, das die Anweisung beendet.

Im folgenden Beispiel ändert das Programm die Anweisung zur Delegatenerstellung dahingehend, dass sie das Listenfeld direkt ändert, statt auf eine Funktion zu verweisen, die das Listenfeld im Auftrag des Programms ändert. Der Code wird gespeichert, um das Listenfeld innerhalb des Ausführungsbereichs zu ändern, und zwar unmittelbar nach der Anweisung zur Delegatenerstellung.

public class MyForm 
{ 
   ListBox listBox; 
   TextBox textBox; 
   Button button; 
   public MyForm() 
   { 
listBox = new ListBox(...); 
textBox = new TextBox(...); 
button = new Button(...); 
button.Click += new EventHandler(sender, e) 
{ 
   listBox.Items.Add(textBox.Text); 
}; 
} 
}

Beachten Sie, wie Code in der Anonymous-Methode auf Variablen, die außerhalb des Codebereichs deklariert wurden, zugreifen und diese ändern kann. In der Tat können anonyme Anonymus-Methoden auf Variablen verweisen, die von der Klasse oder Parametern deklariert wurden, sowie auf lokale Variablen, die in der Methode deklariert wurden, in der sie gespeichert sind.

Übergeben von Parametern an anonyme Methoden
Kurioserweise enthält die Anonymous-Methodenanweisung zwei Parameter namens sender und e. Wenn Sie die Definition des Click-Delegaten der Button-Klasse betrachten, werden Sie feststellen, dass jede Funktion, auf die der Delegat verweist, zwei Parameter enthalten muss: der erste muss vom Typ Object und der zweite vom Typ EventArgs sein. Im ersten Beispiel hat das Programm ohne Verwendung von Anonymous-Methoden zwei Parameter an die AddClick-Methode übergeben, ein Object und ein EventArgs.

Obwohl der Code inline geschrieben wurde, muss der Delegat nach wie vor zwei Parameter empfangen. In der Anonymous-Methode müssen die Namen der beiden Parameter deklariert werden, damit der zugehörige Codeblock sie verwenden kann. Wenn das Click-Ereignis auf der Schaltfläche ausgelöst wird, wird die Anonymous-Methode aufgerufen, und die entsprechenden Parameter werden an sie übergeben.

So funktionieren anonyme Methoden
Wenn ein Anonymous-Delegat auftritt, wandelt der C#-Compiler den Code automatisch innerhalb seines Ausführungsbereichs in eine eindeutig benannte Funktion innerhalb einer eindeutig benannten Klasse um. Der Delegat, in dem der Codeblock gespeichert wird, wird dann als Verweis auf das compilergenerierte Objekt und die Methode festgelegt. Wenn der Delegat aufgerufen wird, wird der Anonymous-Methodenblock über die compilergenerierte Methode ausgeführt.

 

Teiltypen

Beim Programmieren stellt die Beibehaltung des gesamten Quellcodes für einen Typ in einer Datei zwar eine gute objektorientierte Verfahrensweise dar, doch sind die Typen aufgrund von Leistungseinschränkungen manchmal zwangsweise sehr groß. Außerdem sind die Kosten für das Unterteilen des Typs in Untertypen nicht in allen Fällen akzeptabel. Darüber hinaus erstellen oder verwenden Programmierer häufig Anwendungen, die Quellcode ausgeben, wodurch sich der Ergebniscode ändert. Wenn irgendwann ein weiteres Mal Quellcode ausgegeben wird, werden leider alle vorhandenen Quellcodeänderungen überschrieben.

Mit Hilfe von Teiltypen können Sie Typen, die aus großen Mengen von Quellcode bestehen, in mehrere verschiedene Quelldateien unterteilen, wodurch Entwicklung und Wartung erleichtert werden. Darüber hinaus ermöglichen Teiltypen es Ihnen, computergenerierte und benutzergeschriebene Teile Ihrer Typen zu trennen, sodass von einem Tool generierter Code einfacher ergänzt oder geändert werden kann.

Im folgenden Beispiel enthalten zwei C#-Codedateien, File1.cs und File2.cs, Definitionen für eine Klasse namens Foo. Ohne Teiltypen würde dies einen Compilerfehler auslösen, da sich beide Klassen in demselben Namespace befinden. Mit dem Teilschlüsselwort können wir den Compiler darauf hinweisen, dass diese Klasse irgendwo über andere Definitionen verfügt.

File1.cs

File2.cs

public partial class Foo 
{ 
public void MyFunction() 
{ 
// do something here 
} 
}
public partial class Foo{ 
   public void MyOtherFunction()    
   {    
   // do something here    
   } 
}

Beim Kompilieren sammelt der C#-Compiler alle Definitionen eines Teiltyps und kombiniert sie. Die resultierende IL, die vom Compiler generiert wird, zeigt statt mehrerer Klassenbestandteile nur eine kombinierte Klasse.

 

Standardkonformität

Im Dezember 2001 wurde die C#-Programmiersprache von der ECMA (European Computer Manufacturers Association) zum Standard erklärt (ECMA 334). Bald darauf wurde der C#-Standard der ISO (International Organization for Standardization) vorgelegt, von der er voraussichtlich in Kürze ratifiziert wird. Ein bedeutender Meilenstein in der Entwicklung einer neuen Programmiersprache, die Schaffung eines C#-Standards, versprach eine Vielzahl von Implementierungen auf verschiedenen Betriebssystemplattformen. In der Tat hat es sich in der kurzen Geschichte von C# gezeigt, dass eine Reihe von Drittcompileranbietern und Forschern den Standard implementiert und ihre eigenen Versionen des C#-Compilers erstellt haben.

Mit diesen Featurevorschlägen fordert Microsoft Kunden auf, Feedback zu den Zusätzen zur C#-Sprache zu geben. Microsoft beabsichtigt, diese Features in den weiteren Standardisierungsprozess für die Sprache einfließen zu lassen.

 

Verfügbarkeit

Die hier beschriebenen Features werden in einer künftigen Version des C#-Compilers verfügbar sein. Die "Everett"-Version von Visual Studio .NET, die Anfang 2003 auf den Markt kommt, enthält eine leicht geänderte Version von C#, die den ECMA-Standards voll gerecht wird. Die hier beschriebenen Features sind in dieser Version nicht enthalten. Microsoft hofft, dass diese Features in der "VS for Yukon"-Version von Visual Studio enthalten sein werden, für die noch kein Veröffentlichungsdatum festgesetzt wurde.

In den nächsten paar Monaten wird Microsoft weitere Informationen zu diesen Features veröffentlichen, einschließlich vollständiger Spezifikationen. Ansichten und Feedback der Programmierer- und Sprachdesigncommunity zu diesen und anderen interessanten Sprachfeatures sind stets willkommen. Sie erreichen die C#-Sprachdesigner per E-Mail unter sharp@microsoft.com.

Weitere Informationen

C#-Communitywebsite: http://www.csharp.net (in Englisch)
Visual C#