Die .NET Common Language Runtime

Veröffentlicht: 27. Nov 2001 | Aktualisiert: 16. Jun 2004

Von Michael Willers

Das .NET Framework ist der für Entwickler wichtigste Bestandteil der neuen .NET Plattform von Microsoft. Herzstück des Frameworks ist die Common Language Runtime. Grund genug also, ein wenig hinter die Kulissen zu schauen und die Konzepte dieser Laufzeitumgebung näher zu beleuchten. Dieser Artikel gibt einen Überblick und liefert einen Einstieg in die Thematik.

Auf dieser Seite

 Historie
 Codemanager
 Das Common Type System
 Alles ist ein Objekt!?
 Metadaten und Reflection
 Attribute
 Assemblies und Versionierung
 Fazit

Historie

Die meistgestellte Frage bei der Einführung neuer Technologien ist die nach dem "Warum" - Warum also eine gemeinsame Laufzeitumgebung?
Blicken wir dazu ein wenig zurück. Das Anfang der 90er Jahre von Microsoft eingeführte Component Object Model (COM) sollte dazu dienen, die Kommunikation und Integration zwischen Softwarekomponenten zu vereinheitlichen.
Dieses Modell hat sich an Markt durchgesetzt. Das belegen die zahlreichen Lösungen, die heute verfügbar sind - von einfachen visuellen Komponenten (Controls) bis hin zur kompletten SAP-Anbindung.

Die Idee dahinter: Die Integration sollte binär und somit unabhängig von Programmiersprachen erfolgen. Um diese Integration zu ermöglichen bringt COM ein eigenes Typsystem mit. Kurz gesagt: Jede Spache muss neben dem eigenen Typsystem zusätzlich das Typsystem von COM implementieren, um interoperabel zu sein. Diese Vorgehensweise bringt im Wesentlichen drei Probleme mit sich:

  • Die Sprache benötigt einen Layer, der das Typsystem von COM implementiert

  • Der Entwickler muss über diesen Layer die Konvertierungen von "Sprachtypen" in COM-Typen und umgekehrt "von Hand" programmieren

  • Der Entwickler muss neben der Konvertierung zusätzlichen "Infrastrukturcode" für den Layer programmieren, um den Aufrufkonventionen von COM zu genügen

Diese zusätzliche Programmierung macht den Code komplex und extrem fehleranfällig. Wer als C++ Programmierer häufig mit Feldern oder Zeichenketten (COM-Typ SAFEARRAY bzw. BSTR) arbeitet, der weiss wovon hier die Rede ist.

Im Laufe der Jahre ist dieses Modell weiterentwickelt und um Dienste für verteilte Anwendungen, sowie der Möglichkeit für Fernaufrufe (Microsoft Transaction Server, MTS bzw. Distributed COM, DCOM) erweitert worden. Diese Weiterentwicklungen sind mit Windows 2000 zu einem einheitlichen Modell zusammengeflossen, das unter der Bezeichnung COM+ weitläufig bekannt ist.
Aber auch COM+ konnte die Basisproblem nicht lösen: Jede Sprache benötigt weiterhin einen Layer, der das Typsystem von COM implementiert und der Entwickler muss durch zusätzliche Programmierung "seine" Spache an COM anpassen.

netclr01

Bild1: Die Common Language Runtime bietet ein einheitliches Integrationsmodell

Hier setzt die Common Language Runtime (CLR) an und bietet ein einheitliches Integrationsmodell. Die eingangs gestellte Frage nach dem "Warum" kann also wie folgt beantwortet werden: Die Common Language Runtime bietet ein einheitliches Integrationsmodell und dieses Modell sorgt dafür, das die Anwendungsentwicklung konsistenter und einfacher wird.

 

Codemanager

Im .NET Framework spielt der Begriff Managed eine zentrale Rolle. So wird Code, der unter der Regie der Runtime ausgeführt wird mit Managed Code bezeichnet. Das bedeutet, das Aktionen wie das Anlegen eines Objekts oder der Aufruf einer Methode nicht direkt erfolgen sondern an die Runtime delegiert werden. Sie kann dann zusätzliche Dienste wie beispielsweise Versionsüberprüfungen ausführen.
Aus diesem Grund erzeugen die Compiler des Frameworks keinen nativen Code mehr. Vielmehr wird aus dem Quelltext eine prozessorunabhängige Zwischenspache erzeugt, die dann unter Aufsicht der Runtime bei Bedarf zu native Code kompiliert und ausgeführt wird (Just in time Compiler, JIT). Diese Zwischensprache wird mit Microsoft Intermediate Language, MSIL oder kurz IL bezeichnet.

IL-Code wird vor der Ausführung grundsätzlich in echten Maschinencode übersetzt. Somit ist gewährleistet, das immer die schnellstmöglichste Ausführungsgeschwindigkeit gegeben ist. Zudem erlaubt dieses Verfahren eine Entkopplung der Runtime von der zugrunde liegenden Hardware. Ein Vorläufer ist bereits unter Windows CE im Einsatz. Dort erzeugen die Compiler der Entwicklungumgebung auch eine Zwischensprache und erst beim Download auf das CE-Gerät wird die Anwendung in native Code übersetzt und damit hardwareabhängig.

Einzige Ausnahme: Der Visual C++ Compiler. Er auch kann weiterhin native Code erzeugen. Hier ist der Grund allerdings einleuchtend. Nur ein Bruchteil aller Entwickler kommt mit der "rauhen" Wirklichkeit der Programmierung in Berührung: Die Kernel- und Treiberentwickler. Für diese Einsatzgebiete ist eine Runtime denkbar ungeeignet, da in der Regel eine Plattformabhängigkeit vorliegt und maximale Performance zwingend erforderlich ist.

netclr02.gif

Bild2: Sprachintegration erfolgt zukünftig auf Codeebene. Visual C++ kann allerdings weiterhin native Code erzeugen, damit es auch zukünftig noch performante Treibersoftware gibt.

netclr03.gif

Bild3: IL-Code wird durch die JIT-Compiler der Common Language Runtime bei Bedarf in native Code übersetzt und ausgeführt

Die IL ist komplett offengelegt und zur Standardisierung [1] eingereicht worden. Somit kann jeder Compiler, der IL-Code erzeugt, diesen unter Aufsicht der Runtime ausführen lassen. Oder anders gesagt: Dreh- und Angelpunkt der Runtime ist die Integration auf Codeebene.
Ob Sie nun COBOL, Pascal, C# oder Visual Basic benutzen ist egal - solange der Compiler IL-Code erzeugt. Sie können nun beispielsweise eine Klasse in einer Sprache erstellen und mit einer anderen Sprache eine weitere Klasse davon ableiten.
Die Bedeutung, welche Sprache man zur Entwicklung von Anwendungen benutzt, rückt damit in den Hintergrund. Man arbeitet mit der Sprache, die einem am Besten "liegt".

Listing1: Vererbung und einheitliche Fehlerbehandlung über Sprachgrenzen hinweg

' ------------------------------------ 
' Die Basisklasse in VB.NET formuliert 
' Datei BASICDOG.VB 
' ------------------------------------ 
Option Strict Off 
Namespace DogClone 
Public Class BasicDog 
Public Sub Bark(ByVal strName as System.String) 
System.Console.WriteLine("Hello, {0}!",strName) 
End Sub 
Public Sub Ouch 
Dim x as integer = 0 
Dim y as integer = 42 / x 
System.Console.WriteLine("Ouch") 
End Sub 
End Class 
End Namespace 
// ---------------------------------------- 
// Eine abgeleitete Klasse in C# formuliert 
// Datei EXTDOG.CS 
// ---------------------------------------- 
namespace DogClone 
{ 
public class ExtendedDog : BasicDog 
{ 
public void Growl(string strName) 
{ 
System.Console.WriteLine("Hello, {0}. I will bite you!",strName); 
} 
} 
} 
// ------------------------------- 
// Und noch ein Testprogramm in C# 
// Datei DOGDEMO.CS 
// ------------------------------- 
using DogClone; 
class EntryPoint 
{ 
static void Main() 
{ 
try 
{ 
ExtendedDog myDog = new ExtendedDog(); 
myDog.Bark("Michael"); 
myDog.Ouch(); 
myDog.Growl("Simon"); 
} 
catch (System.Exception e) 
{ 
System.Console.WriteLine(e); 
} 
System.Console.ReadLine(); 
} 
}

Allen .NET Compilern ist übrigens gemeinsam, dass sie die umfangreiche Klassenbibliothek der Runtime benutzen. Diese vereinheitlicht die bisherigen Programmierschnittstellen zu einem gemeinsamen Modell und bietet Klassen für nahezu jede "Lebenslage" - von Basisklassen für die Bearbeitung von Zeichenketten bis hin zu Klassen für die Thread-Programmierung. Zugriffe auf das Betriebssystem und das Win32-API erfolgen also nicht mehr direkt sondern werden über Klassen abstrahiert.

Ein weiterer Bereich der Bibiliothek befasst sich mit Fehlerbehandlung - sie erfolgt über alle Sprachen einheitlich in Form von Exceptions!

netclr04.gif

Bild4 : Die Klassenbibliothek im .NET Framework - Ein einheitliches Programmiermodell

 

Das Common Type System

Die Idee der Integration auf Codeebene geht allerdings noch einen Schritt weiter. Die Common Language Runtime stellt allen .NET Sprachen ein umfassendes Typsystem zur Verfügung. Kurz gesagt: Das Typsystem wandert vom Compiler in die Runtime!
Es ist nicht mehr Bestandteil einer Sprache. Vielmehr setzen alle Sprachen auf dem Common Type System, CTS der Runtime auf. Das bedeutet: Typen werden eindeutig, da es nicht mehr verschiedene Repräsentationen ein und desselben Typs gibt - so ist beispielsweise eine Zeichenkette unter Visual Basic .NET identisch mit einer Zeichenkette unter C#!
Typkonvertierungen und Anpassungen an COM-Aufrufkonventionen sind somit nicht mehr erforderlich, wenn Kompontenten unterschiedlicher Sprachen miteinander kommunizieren. Sprachen sind "per Definition" interoperabel, da sie das gleiche Typsystem benutzen!

netclr05.gif

Bild5: Unter COM wird jeder Sprache ein eigener Layer übergestülpt, um interoperabel zu sein. Der Entwickler muss Typkonvertierungen erledigen und sich an die Aufrufkonventionen von COM halten. Die Folge: Komplexer Code und Fehler sind vorprogrammiert.

Für Anwendungsentwickler und Compilerbauer wird es einfacher: Der Anwendungsentwickler wird von fehleranfälligem Konvertierungscode entlastet und der Compilerbauer muss weder Typsystem noch eine Klassenbibliothek implementieren, da beides fester Bestandteil der CLR ist.
Andererseits muss Letzterer dafür sorgen, das bei der Portierung einer bereits bestehenden Sprache auf die CLR ein Mapping von Sprachtypen auf die Typen des Common Type Systems erfolgt .

Aus der Sicht des Anwendungsentwicklers ist dies transparent, er benutzt wie gewohnt "seine" Sprache. Das Mapping erfolgt "behind the scenes" beim Übersetzen durch den Compiler der entsprechenden IL-Code erzeugt (Visual C++.NET ist ein Beispiel für eine solche Portierung).

netclr06.gif

Bild6: Die Common Language Runtime sorgt dafür, das Sprachen per Definiton interoperabel sind und stellt eine entsprechende Infrastruktur bereit. Typkonvertierungen und das Einhalten von COM-Aufrufkonventionen sind nicht mehr notwendig.

 

Alles ist ein Objekt!?

Sämtliche Typen, die über das CTS definiert sind, werden mit Managed Types bezeichnet. Sie werden grundsätzlich von Typ System.Object abgeleitet. Kurz gesagt: Alles ist ein Objekt!

netclr07.gif

Bild7: Das Common Type System - Es gilt die Maxmine "Alles ist ein Objekt"

Dabei stellt sich automatisch die Frage nach der Performance, denn Objekte werden normalerweise stets auf dem Heap abgelegt. Betrachen wir diesen Umstand ein wenig genauer. Grundsätzlich wird zwischen zwei Arten von Typen unterschieden: ValueType und ReferenceType. ValueTypes zeichnen sich durch folgende Eigenschaften aus:

  • sie werden auf dem Stack angelgt

  • sie enthalten Daten

  • sie können nicht dem Wert null annehmen

  • sie repräsentieren im Wesentlichen folgende Typen: Primitive Datentypen wie int, Aufzählungen und Strukturen.

Im Gegensatz dazu gilt für ReferenceTypes:

  • sie werden auf dem Heap angelegt

  • sie enthalten Referenzen auf Objekte

  • sie können den Wert null annehmen

  • sie repräsentieren im Wesentlichen folgende Typen: Zeichenketten, Klassen und Felder.

Für C++ Programmierer ist das ein alter Hut. Allerdings mit einer Ausnahme: Wenn eine Struktur mit new erzeugt wird wird, landet diese auf dem Stack und nicht auf dem Heap! Behalten Sie diese Tatsache bitte im Hinterkopf.

Primitive Datentypen sind also als ValueType definiert und werden somit auf dem Stack abgelegt. Wie aber kann dann ein ValueType eine Objektmethode aufrufen, wenn doch alle Typen von System.Object abgeleitet werden?

Sobald ein ValueType eine Objektmethode aufruft, legt die Runtime automatisch ein temporäres Objekt auf dem Heap angelegt und kopiert den Wert des ValueTypes dort hinein. Dann erfolgt der Methodenaufruf. Nach der Ausführung des Methodenaufrufs wird das Objekt wieder entfernt und man arbeitet mit dem Value Type weiter.

Diese Techniken wurden mit den Begriffen Boxing und Unboxing getauft. Mit Boxing wird das Konvertieren eines ReferenceType in einen ValueType bezeichnet, mit Unboxing der umgekehrte Vorgang.

Man kann Boxing und Unboxing allerdings auch explizit einsetzen, um das Arbeiten mit temporären Objekten zu vermeiden (siehe Bild 8).

netclr08.gif

Bild8: Boxing und Unboxing - Aus der Sicht des Entwicklers sind auch ValueTypes Objekte

Im Hinblick auf die Performance ist diese Vorgehensweise ein guter Kompromiss. Es ist nur dann ein echtes Objekt auf dem Heap vorhanden, wenn es wirklich gebraucht wird. Für den Entwickler ist dies vollkommen transparent. Aus seiner Sicht sind alle Typen Objekte und die Runtime erledigt "den Rest" hinter den Kulissen.
Diese Tatsache ist dann von Bedeutung, wenn Sie eigene Typen implementieren. Sie können so auf einfache Art und Weise "leichtgewichige" Objekte erzeugen. Definieren Sie ihren Typ einfach als Struktur. Sie erinnern sich? Strukturen werden auf dem Stack abgelegt!

 

Metadaten und Reflection

Komponenten, die gegen die Runtime programmiert sind, beschreiben sich selbst. Entsprechende Metadaten werden beim Übersetzen durch den Compiler in die Komponente geschrieben. Der Vorteil: Der Installationsvorgang beschränkt sich auf einfaches Kopieren und zusätzliche Dateien zur Beschreibung wie Headerdateien oder Typenbibliotheken in der COM-Welt sind nicht mehr erforderlich.

netclr09.gif

Bild9: Typen werden immer durch Metadaten beschrieben, die vom Compiler erzeugt werden

Die Metadaten enthalten die Beschreibungen sämtlicher Typen, die in einer Komponente definiert sind. Dazu zählen in der Schnittstellen, Klassen und deren Membervariablen, u.s.w.. Die Membervariablen werden übrigens Felder genannt. Wie aber kommt man an die Metadaten "ran"?
Sobald die Common Language Runtime einen Typ instantiiert (z.B. eine Klasse), wird gleichzeitig ein Objekt vom Typ System.Type angelegt und der soeben erzeugten Typinstanz zugeordnet. Über die Methoden dieses "Typobjekts" können dann die Metadaten ausgelesen werden. Bild 10 zeigt diesen Zusammenhang.

netclr10.gif

Bild10: Der Weg zu Informationen führt nur über das "Typobjekt"

Auf eine detaillierte Betrachtung der Methoden des Reflection-APIs möchte ich hier verzichten und Sie auf die SDK-Dokumentation verweisen. Sie finden diese Methoden unter System.Reflection.

 

Attribute

Richtig interessant wird Reflection erst im Zusammenhang mit den sogenannten "Custom Attributes". Das Common Type System bietet die Möglichkeit, jeden einzelnen Typ zur Entwicklungszeit mit eigenen Metadaten zu versehen. Diese Metadaten werden Attribute genannt und können zur Laufzeit ausgelesen werden. Neben bereits vordefinierten Attributen haben Sie die Möglichkeit, eigene Attribute zu definieren. Attribute werden stets über Klassen implementiert, die sich von der Basisklasse System.Attribute ableiten.

Schauen wir uns dazu ein Beispiel an. Listing 2 zeigt anhand von C#-Code, wie man einen generischen Mechanismus implementieren kann, der Klassen in einer Datenbank ablegt.
Mittels Reflection wird zunächst aus einer Klasse die dazugehörige Tabellendefinition erzeugt. Das erledigt die Methode CreateSchema.
Dabei wird das Mapping zwischen Datenbanktypen und den Typ der einzelnen Klassenmember über das Attribut DBFieldType realisiert. So wird der Code völlig unabhängig von der zugrunde liegenden Datenbank und man legt den zu verwendenden Typ deklarativ (!) fest.
Zum Auslesen der Attributwerte dient die Hilfsfunktion GetAttrValue, die als Übergabeparameter den Typ des Attributs bekommt. Dieser Typ dient als Filter, der dafür sorgt, das wir nur die Attribute zurückgeliefert bekommen, für die wir uns interessieren. Das eigentliche Speichern einer Instanz übernimmt dann die Methode Insert, die das Klasseninstanz als Parameter übergeben bekommt. Und im es "rund" zu machen verpackt man beide Funktionen in eine Klasse SQL-Builder - et voila.

Listing2: Generisches Speichern von Klassen - Reflection machts' möglich

using System; 
using System.Reflection; 
public class DBType : System.Attribute { 
public string val; 
public DBType(string typeName) { this.val = typeName; } 
} 
public interface IBuilder { 
string CreateSchema(Type t); 
string Insert(object o); 
} 
public class SQLBuilder : IBuilder { 
// Wert eines FeldAttributs vom Typ T auslesen 
string GetFieldAttributeValue(FieldInfo f, Type t) { 
foreach(object attr in f.GetCustomAttributes(t)) 
{ 
FieldInfo[] fields = attr.GetType().GetFields(); 
return (fields[0]).GetValue(attr).ToString(); 
} 
return f.FieldType.ToString(); 
} 
// Schemadefinition auf der Basis eines Typs erstellen 
// In diesem Fall eine Tabellendefinition 
public string CreateSchema(Type t) { 
FieldInfo[] fields = t.GetFields(); 
int len = fields.Length; 
string sql = "create table " + t.Name + " ("; 
FieldInfo f; 
for (int i = 0; i < len; i++) 
{ 
f = fields[i]; 
sql += f.Name + " " + GetFieldAttributeValue(f,typeof(DBType)); 
if (i < len-1)  sql += ","; 
} 
sql += ");"; 
return sql; 
} 
// Insert-Statement für Objektinstanz erstellen 
public string Insert(object o) { 
Type t = o.GetType(); 
FieldInfo[] fields = t.GetFields(); 
int len = fields.Length; 
string sql = "insert into " + t.Name + " ("; 
FieldInfo f; 
// Zuerst die Feldnamen ausgeben 
for (int i = 0; i < len; i++) 
{ 
f = fields[i]; 
sql += f.Name; 
if (i < len-1) sql += ","; 
} 
sql += ") values ("; 
// Dann die Feldinhalte ausgeben 
for (int i = 0; i < len; i++) 
{ 
f = fields[i]; 
// Strings gehören in Quotes 
bool b = ((f.FieldType) == typeof(string)); 
if (b) sql += "'"; 
sql += f.GetValue(o).ToString(); 
if (b) sql += "'"; 
if (i < len-1) sql += ","; 
} 
sql += ");"; 
return sql; 
} 
}

Spinnt man diesen Faden weiter, könnte man beispielsweise auf die gleiche Art und Weise einen XML-Builder implementieren und eine Anwendung "bauen", die zur Laufzeit entscheidet, wie eine Klasse abgespeichert werden soll. Entsprechende Klassen für das Arbeiten mit XML finden Sie in der Klassenbibliothek unter System.Xml und System.Xml.Xsl.
Über die Schnittstelle IBuilder wäre die Realisierung einer ClassFactory denkbar, die dann die eine "geeignete" Instanz des Buildes zurückgibt.
Ihrer Phantasie sind hier fast keine Grenzen gesetzt und die Kombination von Reflection mit Attributen bietet viele, spannende Möglichkeiten.
Ein Hinweis noch am Rand: Eine Attributklasse wird erst dann angelegt, wenn der erste Zugriff auf diese Klasse erfolgt, also z.B. beim Aufruf der Methode GetCustomAttributes.

 

Assemblies und Versionierung

Der grösste Problembereich in der COM-Welt ist die Versionierung von Komponenten. Es ist hier nicht möglich, das unterschiedliche Versionen einer Komponente parallel installiert und genutzt werden können. Eine nicht durchdachte Änderung kann weitreichende Folgen haben, da die Änderung sich auf alle Anwendungen auswirkt, die die Komponente benutzen. Dieses Problem ist unter der Bezeichnung "DLL-Hölle" wohlbekannt.

Um diesem Problem zu begegnen, hat Microsoft den Begriff Assembly eingeführt. Unter einem Assembly versteht man vereinfacht gesagt alle Komponenten, die eine Anwendung referenziert. Jedes Assembly verfügt über Metadaten, die die Abhängigkeiten der Komponenten beschreiben. Diese Metadaten werden Manifest genannt. Die einfachste Form ist ein Private Assembly.
Hier werden alle Komponenten in das Verzeichnis der Anwendung oder ein Unterverzeichnis kopiert. Auf diese Art und Weise können verschiedene Anwendungen gleichzeitig mit verschiedenen Versionen einer Komponente arbeiten, ohne sich gegenseitig zu beeinflussen.

Sofern sich die Komponenten der Anwendung in einem oder mehreren Unterverzeichnisse befinden kann man diese über eine Konfigurationsdatei im XML-Format spezifizieren. Der Name dieser Datei muss dem Namen der Anwendung entsprechen und die Endung CFG aufweisen (Wenn ihre Anwendung also beispielsweise TEST.EXE heisst muss die Datei den Namen TEST.CFG haben). Ausserdem müssen sich Konfigurationsdatei und Anwendung im gleichen Verzeichnis befinden.

Listing3: Dateipfade werden über XML-Datei spezifiziert - Administatoren wird's freuen

<?xml version=1.0?> 
<Configuration> 
<AppDomain PrivatePath="<Verzeichnis1>","<Verzeichnis2>"/> 
</Configuration>

Wenn allerdings Komponenten von mehreren Anwendungen benutzt werden sollen, werden diese zu einem Shared Assembly zusammengefasst. Es ist dann möglich, das verschiedene Anwendungen gleichzeitig mit verschiedenen Versionen einer globalen Komponente arbeiten können, ohne sich gegenseitig zu beeinflussen.
Ein Beispiel: Zwei Komponenten bilden ein Assembly und befinden sich im Verzeichnis V1.0.0.1. Dieses Assembly wird von drei unterschiedlichen Anwendungen benutzt. Nun wird eine neuere Version des Assemblies mit neuen Komponenten im Verzeichnis V1.1.0.1 installiert.
Die Anwendungen können dann ebenfalls über eine Konfigurationsdatei bestimmen, mit welcher Assembly-Version sie arbeiten möchten.

Listing4: Anwendungen können gleichzeitig mit verschiedenen Versionen globaler Komponenten arbeiten

<?xml version=1.0?> 
<Configuration> 
<BindingPolicy> 
<BindingRedir 
. . . 
Version ="*" VersionNEW="1.1.0.1" 
UseLatestBuildRevision="no" /> 
. . . 
</BindingPolicy> 
</Configuration>

Allerdings ist dabei zu beachten, dass dann natürlich wieder bestimmte Abhängigkeiten gelten und Sie sehr gut überlegen müssen, wann und wie Sie Komponenten verändern. Sie könnten damit wieder geradewegs in die DLL-Hölle laufen.
Grundsätzlich bleibt aber festzuhalten, dass verschiedene Versionen ein und derselben Komponente mit Hilfe von Assemblies parallel installiert und ausgeführt werden können. Komponenteninformationen sind im Gegensatz zur COM-Welt nicht mehr nur systemweit gültig. Sie können anwendungsspezifisch definiert werden.

 

Fazit

Die Common Language Runtime ermöglicht unabhängig von Programmiersprachen eine durchgängig objekt- und komponentenorientierte Programmierung. Es gibt ein einheitliches Integrationsmodell und die Laufzeitumgebungen verschiedener Sprachen werden durch eine einzige Umgebung ersetzt.
Compiler setzen auf dem Common Type System auf und erzeugen MSIL-Code. Die Integration erfolgt also im Gegensatz zu COM nicht auf binärer Ebene sondern auf Codeebene - Sprachen sind "per Definition" interoperabel!
Die Bedeutung, welche Sprache man zur Entwicklung von Anwendungen benutzt, rückt damit in den Hintergrund. Man arbeitet mit der Sprache, die einem am Besten "liegt". Oder anders formuliert: Sprachen werden gleichwertig und gewinnen an Bedeutung.

Die CLR und die dazugehörige Klassenbibiliothek abstrahieren die Entwicklung von einer konkreten Plattform. Weder COM noch das Win32-API sind zukünftig für den Entwickler sichtbar. Er programmiert gegen die Klassenbibliothek und die Compiler erzeugen prozessorunabhängigen IL-Code als Output.

Ein weiterer wichtiger Punkt: Das .NET Framework basiert nicht auf COM. Aber auch wenn COM nicht mehr benötigt wird, arbeitet das Framework nahtlos mit COM-Komponenten zusammen. Es ist von vornherein auf Interoperatibilität ausgelegt worden. Sie können COM-Komponenten aus .NET Komponenten heraus benutzen und umgekehrt.

Und nicht zuletzt wird die Installation von Anwendungen stark vereinfacht, da die Runtime nicht mehr auf der Registry aufsetzt. Verschiedene Versionen der gleichen Komponente können parallel eingesetzt werden. Die jedem Anwender und Entwickler wohlbekannte "DLL-Hölle" dürfte damit hoffentlich bald der Vergangenheit angehören.

Die Compiler von Microsoft (C#, C++ und Visual Basic) sind Bestandteil des .NET Framework SDKs. Sie müssen also nicht Sie unbedingt auf Visual Studio.NET warten, um beispielsweise erste Gehversuche mit C# zu machen. Ein EMACS tut's auch ;-)

Literatur

[1]Informationen zur Standardisierung der CLR und C# unter https://msdn2.microsoft.com/en-us/netframework/aa569283.aspx