Die .NET CLR - Version 2.0

05.08.2004 von THOMAS WOELFER 
Mit der Version 2 der .NET CLR will Microsoft nächstes Jahr einige in Version 1 vermisste Features wie Generics oder statische Klassen nachreichen. Was Sie von der CLR 2 noch erwarten können und auf welche Änderungen Sie sich einstellen müssen, zeigt dieser Beitrag.

Das erste Beta ist bereits verfügbar - die fertige Version kann im Laufe des nächsten Jahres erwartet werden. Dabei wurde die CLR in Version 2 an vielen Stellen überarbeitet und erweitert. Dieser Beitrag zeigt was es Neues gibt bei der CLR und in der Base Class Library (BCL).

Die Änderungen an CLR und BCL machen sich natürlich primär in den Programmiersprachen bemerkbar: Darum demonstrieren wir alle Neuerungen am Beispiel von C#, der "Hauptsprache" von .NET. Einige der beschriebenen Neuerungen gelten ohnehin ausdrücklich nur für C#.

Zumindest für Programmierer mit einem C++-Hintergrund ist die wichtigste Neuerung ganz klar der Support für die so genannten "Generics". Diese ermöglichen wieder verwendbare Klassen, die im Gegensatz zu den bisherigen Möglichkeiten in .NET auch typensicher sind. Mit anderen Worten: Generics liegt ein ähnliches Konzept zu Grunde, wie den "Templates" in C++. Allerdings: Generics sind keine Templates, sondern wirklich nur verwandt.

Generics werden hauptsächlich für Collections verwendet. Will man in der CLR 1 eine Liste aus Objekten verwalten, dann kann man das auf zwei Arten tun:

Mit Unterstützung von Basisklassen implementiert man eine eigene Listenklasse für den gewünschten Typ, damit die Liste zumindest nach außen typensicher ist.

Man verwendet einfach die ArrayList-Klasse.

Dabei kann man wohl getrost davon ausgehen, dass letzteres Vorgehen in nahezu allen Fällen Verwendung findet, weil ArrayList viel weniger aufwendig ist als die Implementierung einer eigenen Liste.

Fast Templates: Generics

Mit den Generics in der .NET-Laufzeit wird die Arbeit deutlich angenehmer. Zusätzlich gibt es auch noch eine neue Sammlung an Collection-Klassen in der Base Class Library. Die befinden sich im Namespace System.Collections.Generic. Dort findet sich auch eine allgemeine Listenklassen: List<T>.

Der Trick ist nun der, dass man für den Parameter "T" einen beliebigen konkreten Typ einsetzen kann, und dadurch bei der Instanziierung eine typensichere Listenklasse erhält. Angenommen man hat eine Klasse Person und benötigt eine Liste von Personen: Mit Hilfe von Generics und der List<T>-Klasse kann man dann folgendes programmieren:

List<Person> personen;

Die Klasse Person wird später nochmals gebraucht und hat folgenden Aufbau:

public class Person {
private string name;
public string Name {
get {
return name;
}
}
private string vorname;
public string Vorname {
get {
return vorname;
}
}
public Person( string name, string vorname) {
this.name = name;
this.vorname = vorname;
}
}

Die Instanz personen lässt sich nun mit Objekten vom Typ Person füttern. Eine Iteration über diese Liste aus "Person"-Objekten liefert jeweils ein "Person"-Objekt zurück.

Das unterscheidet sich sehr deutlich von der "ArrayList"-Klasse, mit der man zur Zeit noch vorlieb nehmen muss: Dort werden alle Objekte immer als Typ "Object" gespeichert und auch so zurückgeliefert - ganz gleich, welchen konkreten Objekte man in der Liste unterbringt.

Typsicher und schneller

Versucht man nun, ein Objekt vom falschen Typ in der Instanz "personen" abzulegen, führt das zu einer Fehlermeldung des Compilers:

int i = 5;
personen.Add( i); // Hier tritt der Fehler auf

Der Compiler bemängelt, dass der Typ "Integer" nicht in ein Objekt vom Typ "Person" umgewandelt werden kann, und verweigert die Erzeugung von Code.

Die allgemeine ArrayList-Klasse hat aber auch einen anderen Nachteil, der mittels Generics behoben wird: Die Verwendung einer ArrayList kann durchaus zu Performance-Verlusten führen.

Der Grund dafür ist der, dass alle in einer ArrayList untergebrachten Objekte impliziert in ein System.Object umgewandelt werden. Man kann aber auch die so genannten Value-Types (also zum Beispiel "int") in einer ArrayList unterbringen - zu einem Preis: Das implizit durchgeführte boxing - also das Umwandeln des Value-Type in einen Reference-Type - beim Einfügen in die Liste und umgekehrt das unboxing beim Auslesen aus der Liste können durchaus zu einem Performance-Problem werden, wenn diese Operationen zu oft durchgeführt werden.

Bei der generischen List<T>-Klasse ist das nicht länger notwendig, denn hier werden tatsächlich die Value-Types direkt abgelegt.

BCL: Collections schon da

Die Base Class Library versorgt den Programmierer mit den wichtigsten generischen Collection-Klassen. So gibt es neben der Liste auch eine Dictionary-Klasse (eine Hashtable), eine Queue, eine Stack-Klasse und weitere.

Natürlich ist man nicht auf die vorgefertigten Collections beschränkt. Stattdessen kann man eigene Klassen unter der Verwendung von Generics implementieren. Die dazu benötigte Schreibweise entspricht mehr oder minder der in C++: Der Typ-Parameter T (beziehungsweise die Typ-Paramenter, wenn es mehrere davon gibt) werden beim Klassennamen mit angegeben, und können dann als Platzhalter für einen Typ innerhalb der Klasse verwendet werden:

class GenericClass<T> {
private T data;

public GenericClass(T param) {
data = param;
}

public T GetData() {
return data;
}
}

Zusätzlich kann man den Platzhalter "T" mit Beschränkungen (Constraints) versehen. Mit Hilfe dieser Constraints versorgt der Programmierer den Compiler mit zusätzlichen Informationen, über die dann weitergehende Operationen mit "T" ermöglicht werden. Ein solches Constraint ist beispielsweise das Basisklassen-Constraint. Dabei gibt man an, dass alle Instanzen von "T" von einer bestimmten Basisklasse (oder davon abgeleiteten Klassen) sind, sodass man innerhalb der Generic-Klasse Methoden der Basisklasse von T verwenden kann.

Für Typsicherheit: Constraints

Constraints werden mit dem Schlüsselwort "where" eingeführt. Um in der "GenericClass" beispielsweise sicherzustellen, dass nur von "Person" abgeleitete Klassen verwendet werden, schreibt man:

class GenericClass<T> where T : Person {

Dadurch wird sichergestellt, dass es sich bei T um eine "Person" (oder eine davon abgeleitete Klasse) handeln muss - und das ermöglicht es dann, innerhalb von "GenericClass" die Methoden von "Person" zu verwenden. Angenommen, man will im Falle eines bestimmten Namens ("foo") eine Sonderbehandlung einführen, so kann man dann programmieren:

class GenericClass<T> where T:Person {
private T data;

public GenericClass(T param) {
data = param;
}

public T GetData() {
if (data.Name == "foo") return null;

return data;
}
}

Generische Methoden

Man kann Generics aber nicht nur bei der Definition einer Klasse sondern auch bei Methoden verwenden. Eine Methode mit Typ-Parameter nennt man "Generic Method". Ähnlich wie bei der Klasse wird der Typ auch bei einer generischen Methode in spitzen Klammern mit angegeben. Das typische "Swap"-Beispiel zum Vertauschen zweier Objekte sieht also mit Generics wie folgt aus:

private void GenericSwap<T>(ref T a, ref T b) {
T temp = a;
a = b;
b = temp;
}

Beim Aufruf einer solchen Funktion ist es nicht notwendig, den gewünschten konkreten Typ anzugeben, da der Compiler diesen aus den verwendeten Parametern abliest:

int i1 = 5;
int i2 = 6;
GenericSwap(ref i1, ref i2);

Ein Problem bei der Verwendung von Generics ist die Initialisierung mit einem Null-Wert: Handelt es sich bei T nämlich um einen Reference-Type, dann müsste mit "=null" initialisiert werden, im Falle eines Value-Types mit "=0".

Dieses Problem wird (in C#) mit dem Schlüsselwort "default" umgangen. Das Schlüsselwort liefert "null", wenn es im Zusammenhang mit einem Reference-Type verwendet wird und "0" bei einem Value-Type:

T data;
data = default( T);

Auch wenn die bisherigen Beispiele diesen Eindruck erweckt haben: Generics in CRL2 sind keine C++-Templates. Es gibt sogar sehr viele Unterschiede zwischen C++-Templates und CLR-Generics. Die wichtigsten stellen wir Ihnen im Folgenden vor.

Templates und Generics: Die Unterschiede

Der wichtigste Unterschied ist sicherlich die Tatsache, dass die Spezialisierung einer .NET-Generics-Klasse zur Laufzeit stattfindet, wohingegen C++-Templates beim Compilieren spezialisiert werden. Das klingt komplizierter als es in Wirklichkeit ist - hier ein Beispiel.

Angenommen Sie haben ein C++-Template vom TypList<T>. Wenn Sie dieses Template nun in C++ verwenden wollen, um eine Liste aus Integern anzulegen, so werden Sie folgenden Ausdruck verwenden.

List<int> aList;

Wenn der Compiler zum ersten Mal auf diesen Ausdruck stößt, wird er den Quellcode von List<T> abarbeiten und mit Hilfe des konkreten Types int effektiv einen neuen Typ List<int> erzeugen - mit dem kompletten zugehörigen Quellcode auf Basis der Template-Klasse.

Das passiert in C++ in jeder Übersetzungseinheit. Wenn Sie nun (es geht hier um .NET-Programmierung mit C++) zwei Assemblies haben, und beide benutzenList<int>, dann stoßen Sie auf Probleme: In .NET ist es nämlich so, dass die erzeugende Assembly ein Teil des Typs einer Klasse ist. Mit anderen Worten: List<int> aus der einen Assembly ist ein anderer Typ als List<int> aus der anderen Assembly!

Bei .NET Generics läuft die Sache anders: Hier findet die Spezialisierung erst zur Laufzeit statt - und zwar auf Basis der bereits übersetzen Template-Klasse. Diese befindet sich also in einer Assembly - und der Typ der spezialisierten, zur Laufzeit erzeugten Klasse wird dieser Assembly zugeordnet. Mit anderen Worten: Bei .NET Generics sind spezialisierten Templates über alle Assemblies hinweg nutzbar.

.NET Generics vs. C++-Template

Es gibt noch einen anderen deutlichen Unterschied, der ebenfalls mit der Spezialisierung von Templates zu tun hat. Bei einem C++-Template können Sie beispielsweise den folgenden Code programmieren:

static T Max<T>( T v1, T v2) {
if( v1.CompareWith( v2) > 0) {
return v1;
}
return v2;
}

Dabei wird davon ausgegangen, dass alle Typen, für die Max<T> verwendet wird, auch eine Methode CompareWith() haben. Verwenden Sie später bei der Arbeit Max<T> für einen Typ, der kein CompareWith definiert, so erhalten Sie eine Fehlermeldung des Compilers: Aber eben erst dann, wenn Sie Max<T> benutzen.

Bei Generics ist die Sachlage anders: Dort wird der Template-Code bereits erzeugt, bevor die Spezialisierung stattfindet. Dieser Code muss typsicher sein, damit die Konventionen eingehalten werden. Allerdings hat der Compiler keine Möglichkeit, typsicheren Code für das Beispiel zu erzeugen, da er nicht sicherstellen kann, dass die Methode CompareWith() vorhanden ist. Dementsprechend können Sie den Beispielcode mit Generics erst gar nicht übersetzen.

Um ein solches Konstrukt zu ermöglichen, müssen Sie bei Generics auf Constraints zurückgreifen: T muss also so definiert werden, dass die Methode CompareWith() auch enthalten ist. Das geht zum Beispiel über ein Interface-Constraint, oder über das bereits zuvor angesprochene Basisklassen-Constraint.

Weitere relevante Unterschiede zwischen Generics und Templates sind die folgenden:

Generics sind weniger flexibel als Templates: So kann man zum Beispiel keine arithmetischen Operatoren innerhalb einer Generics-Klasse aufrufen.

Im Gegensatz zu Templates können bei Generics keinen Template-Parameter verwendet werden, die kein Typ sind. Das folgende C++-Beispiel ist also mit Generics nicht möglich:

template Beispiel< int i> {}

Praktisch: Anonyme Methoden

Das nächste sehr interessante Feature aus der CLR2 sind so genannte Anonyme Methoden. Diese lassen sich im Wesentlichen überall dort verwenden, wo auch Delegates zum Einsatz kommen. Der Trick dabei: Statt der tatsächlichen Implementierung einer Delegate-Methode übergibt man einfach nur den Quellcode, der ausgeführt werden soll.

Damit lassen sich Delegates in vielen Fällen sehr viel einfacher verwenden. Angenommen, Sie möchten einen Click-Event mit einem Handler ausstatten, der Handler soll aber keine Methode einer Klasse, sondern eben eine "Anonyme Methode" sein. In diesem Fall würden Sie das im Quellcode wie folgt ausdrücken:

button2.Click += delegate { MessageBox.Show("hiho Silver!"); };

Im Beispielprojekt wird dieser Quellcode in den Handler des Click-Events von Button1 eingebaut: Ein Click auf Button1 erweitert also die Liste der Handler von Button2. Klickt der Benutzer danach auf Button2, wird der Code mit der MessageBox aufgerufen: Es erscheint also eine Nachricht mit dem Text "hiho Silver!".

Man kann sich den Mechanismus also so vorstellen, dass man in der neuen Version der CLR nun einen Codeblock als Parameter übergeben kann. Dabei ist es aber interessanterweise möglich, auf Variable zuzugreifen, die außerhalb des Codeblocks liegen. Die äußeren Variablen ("outer variables") stehen also ganz normal zur Verfügung - wenn auch mit minimal veränderten Scoping-Regeln. Folgender Code ist also zulässing:

private int calls = 0;

private void button1_Click(object sender, EventArgs e) {
calls++;
button2.Click += delegate { MessageBox.Show("hiho Silver! - " + calls.ToString() );
}

Neu in C#: Statische Klassen

Zumindest C/C++-Programmierer haben das Keyword "static" in C# schmerzlich vermisst: Mit der CLR2 kehren nun statische Klassen ins Leben des Programmierers zurück. Statische Klassen sind solche Klassen, die nicht instanziiert werden können, die nur statische Member haben und deren Methoden ohne eine bestimmte Instanz der Klasse aufgerufen werden können.

Mit der alten CLR kann man sich in C# damit behelfen, solche Klassen als sealed zu kennzeichnen, und mit einem privaten Konstruktor zu versehen. Dieser Workaround ist nun nicht länger notwendig.

Ein Beispiel für eine statische Klasse - hier ein einfacher Converter - könnte also folgenden Aufbau haben:

static class ToUpperConverter {
static string Convert(string a) {
string b = a.ToUpper();
return b;
}
}

Neue und neue alte Klassen in der BCL

Mit der neuen Version der Runtime kommt auch eine neue Version der BCL daher - und diese zeichnet sich in erster Linie durch eine Vielzahl neuer Klassen aus. Dabei gibt es einige neue Namespaces aber auch Erweiterungen der bisher bekannten Namespaces.

Ein Beispiel sind die neuen "Generics"-Collections aus dem Namespace System.Collections.Generic. Als populär dürften sich aber auch die neuen Klassen aus Windows.Forms erweisen, der um eine ganze Reihe neuer Controls erweitert wurde, mit denen sich Anwendungen deutlich einfacher als bisher um zusätzliche Funktionalitäten erweitert lassen.

Ein solches neues Control ist das Webbrowser-Control. Dabei handelt es sich um einen Managed Code Wrapper um einen Teil der Internet Explorer API . Damit lassen sich HTML-Inhalte einfach in eigenen Windows.Forms-Anwendungen darstellen und zwar ohne den Rückgriff auf Interop.

Ein weiteres interessantes Control ist DataGrid-View, das deutlich mehr Tricks beherrscht als sein Vorgänger (DataGrid). So bietet es zum Beispiel mehr Zell-Typen und lässt sich erheblich einfacher erweitern. Der Vorgänger ist aus Kompatibilitätsgründen natürlich immer noch Teil der BCL - nur eben einer, der als "veraltet" gilt.

SplitContainer und MaskEdit

Das DataGrid-Control ist dabei nicht das einzige Control, das durch neuen Code ersetzt wurde: Gleiches ist auch dem Splitter-Control passiert, das durch den SplitContainer ersetzt wird. Der SplitContainer besteht aus zwei Panels, die durch einen Splitter getrennt sind. Statt also wie früher erst umständlich zwei Panel und einen Splitter auf ein Form zu ziehen, geht das bei Windows.Forms 2 in einem Schritt.

Natürlich kann man das SplitContainer-Control auch konfigurieren. So ist es ein Einfaches zwischen horizontaler und vertikaler Ausrichtung umzuschalten - ein Vorgang, der mit dem Splitter-Control aus den alten Windows.Forms durchaus Kopfschmerzen bereitet.

Ein völlig neues Control für die BCL ist das "Masked Edit Control". Programmierer, die sich mit Visual Basic oder Access beschäftigt haben, werden einen Verwandten allerdings bereits kennen: Das "Masked Editor Control" ist ein Edit-Control, das man mit einer Maske versehen kann - es nimmt dann nur noch Eingaben entgegen, die zu dieser Maske passen, beispielsweise nur Zahlen oder nur Großbuchstaben.

Damit kann man relativ leicht sicherstellen, dass eingegebene Daten einem gewünschten Format entsprechen, und zwar ohne eigene aufwendige Tests durchzuführen.

ToolStrip und MenuStrip

Auch für Toolbar und MenuBar gibt es Ersatz in Windows.Forms2 - und zwar in Form der Controls MenuStrip und ToolStrip. Das interessantere davon ist ToolStrip, mit dem sich die von Office (und Visual Studio) bekannten Toolbars und Menuleisten erstmals in eigenen Programmen implementieren lassen.

Das ToolStrip-Control kann dabei die Aufgaben einer normalen Werkzeugleiste übernehmen oder als Menü fungieren. Es lässt sich an den Rand des Container-Form andocken oder frei "floaten". Bringt man es am linken oder rechten Rand des Form an, so werden die Beschriftungen der Befehle automatisch gedreht - Befehle können (müssen aber nicht) zugeordnete Icons haben, und so weiter und so fort. Mit einem Wort: Das ToolStrip-Control bietet all die Funktionen, die man sich schon seit Jahren von einem ToolBar-Control gewünscht hat. Bisher bekam man diese aber nur in Form von (meist kostenpflichtigen) Libraries von Drittherstellern, nicht aber von Windows selbst.

Der ActiveDocument-Host

Der ActiveDocument-Host ist ein Control, das der Funktion des WebBrowser-Controls stark ähnelt - man kann nämlich andere Anwendungen darin anzeigen. Das WebBrowser-Control ist allerdings ausschließlich für HTML-Seiten zuständig und kapselt im Wesentlichen die API des Internet Explorer. Der ActiveDocument-Host dagegen kümmert sich um "aktive" Dokumente. Unter dieser Bezeichnung sind OLE-Dokumente zu verstehen, er "hostet" also alle OLE-fähigen Dokumente.

Dazu gehören zum Beispiel mit Anwendungen von Microsoft Office erstellte Dokumente, aber auch Bilder aus PaintShop Pro, PDF-Dateien und vieles mehr. Die Verwendung des Hosts ist denkbar einfach. Man bettet das Control in einem Form ein, und teilt ihm dann mit, welches OLE-Dokument angezeigt werden soll. Dabei kann es sich um ein neues Dokument handeln oder auch ein bereits vorhandenes.

Layout-Panel

Mit dem TableLayout- und dem FlowLayout-Panel erhält Windows.Forms 2.0 zwei neue Panel-Element für das Layout von Forms. Wie der Name schon vermuten lässt, erlaubt das FlowLayout-Panel ein fließendes, dynamisches Layout - was im Großen und Ganzen der Arbeitsweise von HTML entspricht: Wird das Form breiter gezogen, rutschen zuvor untereinander platzierte Elemente nebeneinander nach oben. Der Effekt ist mit einer einfachen HTML-Seite nachzuvollziehen, die keine Tabellen verwendet.

Ähnliches gilt für das TableLayout-Panel: Dieses Control ermöglicht ein Layout, das der Verhaltensweise von Tabellen in HTML entspricht. Dabei verbleiben alle Elemente relativ zueinander an der gleichen Position, innerhalb einer Tabellen-Zelle.

Je nach Anwendungsfall sind beide Panels sehr praktisch, wenn man Dialoge programmieren möchte, deren Größe zur Laufzeit dynamisch verändert werden kann.

Neues in C#: Iteratoren

In C# 2.0 existieren ebenfalls neue Elemente, die die Arbeit mit der CLR und der BCL einfacher und angenehmer gestalten. Dazu gehören Iteratoren, Nullable Types und Partial Class Definitions.

Bei einem Iterator handelt es sich im Wesentlichen um eine Methode, mit der man die Schleife foreach auf eine Collection anwenden kann. Das ist für .NET prinzipiell nichts Neues: Bisher musste man händisch die Schnittstellen IEnumerable oder IEnumerator implementieren, um diese Möglichkeit zu erhalten. Mit C# 2.0 geht das deutlich einfacher, denn hier muss man nur noch eine Methode implementieren, die eine der generischen Iterator-Schnittstellen - also zum Beispiel IEnumerator - als Rückgabewert liefert.

Im Code-Block dieser Funktion verwendet man dann das neue Statement "yield return". Dieses legt fest, welcher Wert geliefert werden soll, und kümmert sich außerdem darum, den aktuellen Wert der Iteration zu speichern. Beim nächsten Aufruf des Codeblocks läuft die Iteration dann an dieser gemerkten Stelle weiter.

Angenommen, Sie haben eine Klasse, die die Tage der Woche kapselt, und über diese Tage soll mit foreach iteriert werden. Der bei C# 2.0 benötigte Code sieht unter Verwendung von Iteratoren dann wie folgt aus:

public class Wochentage {
string[] tage = { "Montag", "Dienstag", "Mittwoch", "Sudeltag", "Freitag", "Samstag", "Sonntag" };

public System.Collections.IEnumerator GetEnumerator() {
foreach( string tag in tage)
yield return tag;
}
}

Über eine Instanz vom Typ "Wochentage" kann dann ganz einfach mit foreach iteriert werden:

Wochentage t = new Wochentage();
foreach( string tag in t) {
MessageBox.Show( tag);
}

Neues in C#: Partial Class Definitions

"Partial Class Definitions" sind ein in allen Vorgängerversionen von C# schmerzlich vermisstes Feature: Bisher musste eine Klasse mit allen Methoden vollständig in einer Datei enthalten sein. Das hat zwei unschöne Auswirkungen: Automatisch erzeugter Code, wie zum Beispiel der des Forms-Designer, landen unweigerlich in der gleichen Quellcode-Datei, wie der vom Entwickler programmierte Code. Das schafft Verwirrung und macht den Code unübersichtlich - schöner wäre es, wenn der automatisch erzeugte Code in einer separaten Datei abgelegt würde.

Das zweite Problem tritt bei umfangreichen Klassen auf: Hier ist es aus Gründen der Ordnung oftmals wünschenswert, einzelne Methoden einer Klasse in separaten Dateien unterzubringen.

Beides ist nun möglich, und dazu gibt es das neue Schlüsselwort partial. Die Verwendung ist ganz einfach: Man teilt die Klasse einfach in beliebig viele Dateien auf - und stellt in allen Dateien das Schlüsselwort partial vor das Schlüsselwort class.

Neues in C#: Nullable Types

Nullable Types sind Datentypen, die alle Werte des zugrunde liegenden Typen annehmen können - und zusätzlich den Wert null. Somit kann man Variable erzeugen, bei denen über den Wert null signalisiert, dass sie noch nicht initialisiert wurden, beispielsweise eine Integer-Variable:

int? var;
if( var == null) Console.WriteLine("Nicht initialisiert");
else Console.WriteLine( var.Value);

Nullable Types sind als Generics gelöst: Ein nullable Type ist vom TypSystem.Nullable<T>, bei dem <T> den Typ angibt. In C# gibt es dafür eine andere abkürzende Schreibweise, und zwar T?. Man schreibt also den eigentlichen Typ gefolgt von einem "?".

Nullable Types besitzen zwei Properties, und zwar HasValue und Value. Ersteres liefert true, wenn die Variable einen Wert zugewiesen bekommen hat, und Value liefert den eigentlichen Wert.

Fazit

Auf Programmierer kommen viele neuen Features zu: Die Common Language Runtime hat mehr Tricks drauf, die Base Class Library enthält eine Vielzahl von neuen Klassen und beides macht sich natürlich in den .NET-Sprachen bemerkbar.

Wer mag, muss nicht bis nächstes Jahr warten, um die neuen Möglichkeiten auszuprobieren - auf msdn.microsoft.com gibt es bereits eine frühe Beta-Version von .NET 2.0 und mit den "Express"-Versionen der IDEs für Visual Basic, C# und C++ auch Entwicklungsumgebungen, die jedermann zum Ausprobieren verwenden kann.

In einem Folgeartikel werden wir uns ausführlich der neuen Entwicklungsumgebung Visual Studio 2005 widmen. (mha)