Die neue Programmiersprache C#

Veröffentlicht: 12. Feb 2001 | Aktualisiert: 14. Jun 2004

Von Joshua Trupin

Auf dieser Seite

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild01

Dieser Beitrag soll dazu dienen, Ihnen einen schnellen Überblick zu verschaffen, was C# ist und warum Sie sich damit auseinandersetzen sollten. Wenn Sie bereits Details der Sprache kennen lernen wollen, dann lesen Sie den Artikel von Marcellus Buchheit in dieser Ausgabe.
C# wurde unter anderem mit dem Ziel entwickelt, die Entwicklung und Wartung von Programmen zu vereinfachen. Das Ergebnis wirkt so, als hätten sich die Entwickler bemüht, die guten Dinge aus Visual Basic in C++ einzubauen und mit den schwierigeren Traditionen von C und C++ zu brechen.
Und nun erwartet man, dass sich C# zur besten Sprache für die Erstellung von .NET-Anwendungen für den kommerziellen Einsatz in Unternehmen entwickeln wird. Allerdings brauchen Sie nicht den vorhandenen C- oder C++-Code auf C# umzustellen. Wenn Sie das Leistungsangebot von C# für akzeptabel halten - und Sie werden es lieben - können Sie in gewisser Weise Ihr Weltbild auf C# umstellen. Wie Sie sicher wissen, ist C++ eine überaus leistungsfähige Sprache, aber vom Schwierigkeitsgrad her nicht gerade ein Erholungsurlaub. Ich habe beruflich mit Visual Basic und mit C++ gearbeitet und mich nach einer gewissen Zeit gefragt, warum ich eigentlich auch für die unwichtigste C++-Klasse immer wieder die üblichen Destruktoren implementieren musste. C++, Du bist eine schlaue Sprache. Und Visual C++ hat sogar IntelliSense. Warum also räumst Du nicht hinter mir auf?
Nun, wenn Sie gerne mit C und C++ arbeiten, aber gelegentlich so denken wie ich, ist C# die Sprache für Sie.
Das wichtigste Entwurfsziel war nicht die reine Leistungsfähigkeit, sondern die einfache Anwendung. Man tauscht etwas Rechenleistung gegen Typsicherheit und eine automatische Speicherbereinigung (Garbage Collection) ein. C# kann zur Codestabilität und zur Steigerung Ihrer Produktivität beitragen. Auf lange Sicht machen Sie also die verlorene Rechenleistung mehr als wett. In Schlagworten könnte man C# folgendermaß;en umschreiben:

  • Einfache Anwendung

  • Einheitliches Typsystem

  • Modernes Konzept

  • Objektorientierung

  • Typsicherheit

  • Skalierbarkeit

  • Versionsunterstützung

  • Kompatibilität

  • Flexibilität

Schauen wir uns nun etwas genauer an, wie C# das Programmiererleben erleichtert.

Einfache Anwendung

Was stört die meisten Programmierer an C++ am stärksten? Man muss sich merken, wann der Pfeil -> anzuwenden ist, wann ein Klassenelement mit :: adressiert wird und wann der Punkt gefragt ist. Und der Compiler merkt meistens, wann Sie schief liegen, nicht wahr? Er sagt Ihnen sogar noch, dass Sie einen Fehler gemacht haben. Wenn es dafür einen Grund gibt, der über reine Spottlust hinausgeht, bin ich wohl nicht in der Lage, ihn zu erkennen.
C# erkennt diese kleinen, aber lästigen Hindernisse des C++ Programmiererlebens und vereinfacht den Sachverhalt. In C# wird alles durch den Punkt dargestellt. Ob Sie nun mit Datenelementen hantieren, mit Klassen, Namensräumen, Referenzen oder was auch immer, Sie brauchen sich nicht mehr zu merken, welcher Operator nun der richtige ist.
So weit, so gut. Was ist das nächste lästige Ärgernis, mit dem man sich in C und C++ herumschlägt? Nun, man muss immer exakt herausfinden, welcher Datentyp einzusetzen ist. In C# ist ein Unicode-Zeichen kein wchar_t, sondern einfach ein char. Eine ganze Zahl mit 64 Bits ist ein long und kein __int64. Und ein char ist ein char ist ein char. Es gibt nicht mehr diesen Teilchen-Zoo mit char, unsigned char, signed char und wchar_t, bei dem jedes hochwohlgeborene Mitglied beleidigt ist, wenn man es mit anderen verwechselt. (Auf die Datentypen komme ich später noch zurück.)
Das dritte lästige Problem, auf das man in C und C++ stöß;t, ist der Einsatz von Integern als logische Werte, was zu den allseits beliebten Zuweisungsfehlern führt, wenn man = und == verwechselt. C# trennt diese beiden Typen und bietet einen separaten Logiktyp an, der dieses Problem löst. Ein bool kann true oder false sein und lässt sich nicht in andere Typen konvertieren. Daher lässt sich zum Beispiel ein Integer oder eine Objektreferenz nicht mehr auf true oder false testen - er muss mit null verglichen werden. Wenn Sie in C++ zum Beispiel gerne solche Konstruktionen schreiben wie

int i; 
if (i) . . .

ist für den Umstieg auf C# eine kleine Änderung fällig:

int i; 
if (i != 0) . . .

Ebenso programmiererfreundlich dürfte die Art und Weise sein, wie die switch-Anweisungen in C# funktionieren. In C++ kann man eine switch-Anweisung so aufbauen, dass das Programm von case zu case durchrutscht. So würden die folgenden Zeilen zum Beispiel zum Aufruf von FunctionA() und FunctionB() führen, falls i gleich eins ist:

switch (i) 
{ 
    case 1: 
        FunctionA(); 
    case 2: 
        FunctionB(); 
        Break; 
} 

Einheitliches Typsystem

C# vereinfacht das Typsystem dadurch, dass man jeden Datentyp als Objekt betrachten kann. Ob es nun um eine Klasse geht, um eine struct, ein Array oder einen Grundtypen, alle lassen sich wie Objekte benutzen. Objekte werden in Namensräumen zusammengefasst, so dass alles vom Programmcode aus zugänglich ist. Man nimmt also nicht länger Header-Dateien wie diese ins Programm auf:

#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 

Statt dessen nehmen Sie einen bestimmten Namensraum ins Programm auf, um Zugriff auf die darin enthaltenen Klassen und Objekte zu erhalten:

using System; 

Im .NET liegen alle Klassen in einem einzigen hierarchischen Namensraum. In C# können Sie mit der Anweisung using dafür sorgen, dass Sie für den Zugriff auf eine Klasse nicht mehr den vollständigen qualifizierten Namen angeben müssen. So enthält der Namensraum System zum Beispiel eine ganze Reihe von Klassen, darunter auch die Klasse Console. Console hat eine Methode namens WriteLine, die, wie wohl jeder schon vermutet, eine Zeile auf die Systemkonsole schreibt. Wenn sie also in einem absolut kreativen und revolutionär neuen Hallo-Welt-Programm den erforderlichen Text auf den Bildschirm bringen möchten, kann die Formulierung in C# so aussehen:

System.Console.WriteLine("Hello World!"); 

Derselbe Code ließ;e sich auch so schreiben:

Using System; 
Console.WriteLine("Hello World!"); 

Und das ist fast schon alles, was Sie für ein lauffähiges Plagiat der berühmten Vorlage brauchen. Ein vollständiges C#-Programm braucht eine Klassendefinition und eine Main-Funktion. Folglich könnte ein vollständiges Hello-World-Konsolenprogramm in C# so aussehen:

using System; 
class HelloWorld 
{ 
    public static int Main(String[] args) 
    { 
        Console.WriteLine("Hello, World!"); 
        return 0; 
    } 
} 

Die erste Zeile macht den Namensraum System im Programm verfügbar. Das ist der Basis-Namensraum von .NET. Die eigentliche Programmklasse erhält den Namen HelloWorld. Der Code wird also in Klassen organisiert, also nicht primär in Dateien. Die Main-Methode, die auch Argumente annehmen kann, wird innerhalb der Klasse HelloWorld definiert. Die .NET-Klasse Console trägt die freundliche Meldung auf den Bildschirm - und fertig ist das Programm.
Natürlich kann man auch mehr Aufwand treiben. Wie sieht es eigentlich aus, wenn Sie das HelloWorld-Programm wiederverwenden möchten? Nun, das ist simpel. Bringen Sie es einfach in seinem eigenen Namensraum unter! Verpacken Sie das Programm in einem Namensraum und deklarieren Sie die Klasse als public, wenn sie auß;erhalb des betreffenden Namensraums zugänglich sein soll. (Beachten Sie bitte, dass ich den Namen Main hier in das passendere SayHi geändert habe.)

using System; 
namespace SystemJournal 
{ 
    public class HelloWorld 
    { 
        public static int SayHi()  
        { 
            Console.WriteLine("Hello, World!"); 
            return 0; 
        } 
    } 
} 

Nun können Sie diese Konstruktion zu einer DLL kompilieren und die DLL in jedes andere Programm einbinden, das Sie entwickeln. Das aufrufende Programm könnte zum Beispiel so aussehen:

using System; 
using SystemJournal; 
class CallingSystemJournal 
{ 
    public static void Main(string[] args)  
    { 
        HelloWorld.SayHi(); 
        return 0; 
    } 
} 

Eine letzte Anmerkung zum Thema Klassen. Wenn in mehreren Namensräumen Klassen mit demselben Namen liegen, können Sie in C# nun für jede einen passenden Zweitnamen (alias) definieren, damit Sie die Bezüge nicht vollständig zu qualifizieren brauchen. Hier ein Beispiel. Nehmen wir an, Sie hätten eine Klasse NS1.NS2.ClassA entwickelt, die so aussieht:

namespace NS1.NS2 
{ 
    class ClassA {} 
} 

Nun können Sie einen zweiten Namensraum NS3 definieren, der folgendermaß;en die Klasse N3.ClassB von NS1.NS2.ClassA ableitet:

namespace NS3 
{ 
    class ClassB: NS1.NS2.ClassA {} 
} 

Falls Ihnen diese Konstruktion zu lang ist oder sie in der restlichen Anwendung ständig zitiert werden soll, können Sie folgendermaß;en für die Klasse NS1.NS2.ClassA den Zweitnamen A definieren:

namespace NS3 
{ 
    using A = NS1.NS2.ClassA; 
    class ClassB: A {} 
} 

Die Definition kann sich auf jede Ebene der Objekthierarchie beziehen. So können Sie zum Beispiel auch einen Zweitnamen für NS1.NS2 definieren:

namespace NS3 
{ 
    using C = NS1.NS2; 
    class ClassB: C.A {} 
} 

Modernes Konzept

Die Anforderungen der Entwickler an die Programmiersprachen werden im Lauf der Zeit immer höher. Was einst geradezu revolutionär war, ist inzwischen - sagen wir - gealtert. Wie der gute, alte Toyota Corolla in Nachbars Garten bieten C und C++ zwar eine überaus zuverlässige Transportgelegenheit, aber ihnen fehlen so die letzten blinkenden und glitzernden Spielsachen, nach denen die Leute lechzen, wenn sie richtig Gummi geben wollen. Das dürfte wohl einer der Gründe sein, warum viele Entwickler in den letzten Jahren zunehmend mit Java herumspielen.
C# geht in gewissem Sinne wieder zurück ans Zeichenbrett und kommt dann mit einigen Ideen zurück, die ich in C++ schon lange vermisse. Die gute, alte Speicherbereinigung (garbage collection) ist ein Beispiel dafür. Alles wird irgendwann vom System weggeräumt, wenn es nicht mehr gebraucht wird. Allerdings hat solch ein Luxus natürlich seinen Preis. Bestimmte Probleme, die sich aus Programmierfehlern ergeben (zum Beispiel aus falschen Typkonvertierungen oder streuenden Zeigern), sind unter Umständen wesentlich schwerer zu erkennen und können, wenn man Pech hat, im Programm wesentlich mehr Schaden anrichten. Um dem entgegenzusteuern, bietet C# eine gewisse Typsicherheit an, durch die sich die Stabilität der Programme erhöhen soll. Natürlich wird der Code durch die Typsicherheit auch leichter lesbar, so dass auch die anderen aus dem Team leichter erkennen, was Sie eigentlich treiben - nun, man kann das eine wohl nicht ohne das andere haben, denke ich mal. (Darauf komme ich später noch zurück.)
C# hat von Haus aus umfangreichere Vorkehrungen zur Bearbeitung von Fehlern als C++. Haben Sie sich bei der Fehlersuche schon mal so richtig tief in den Code Ihrer Kollegen hineingewühlt? Es ist schon erstaunlich. Da sind Dutzende von ungeprüften HRESULTs über die Zeilen verstreut und wenn ein Aufruf fehlschlägt, outet sich das Programm mit einer genialen Fehlermeldung wie "Fehler: Es gab einen Fehler." C# versucht, diese Situation durch die Einbindung von Konstruktionen wie throw, try..catch und try..finally als Sprachelemente zu verbessern. Sicher, in C++ könnte man das alles als Makro einbauen, aber nun bietet es die Programmiersprache eben von Haus aus an.
Zu einer modernen Programmiersprache gehört, dass sie tatsächlich für irgendwelche Problemlösungen zu gebrauchen ist. Bitte? Sie lachen? Nun, so simpel und naheliegend diese Forderung auch zu sein scheint, so gerne wird sie ignoriert. Viele Sprachen kennen zum Beispiel keine Währungstypen und können auch mit Zeit- und Datumsangaben nichts anfangen. Solche Ideen sind den Sprach-Genies wohl zu sehr "Old Economy". In Anlehnung an Sprachen wie SQL implementiert C# Datentypen wie Dezimalzahlen und Strings und ermöglicht zudem die Erweiterung des Typsystems durch neue Grundtypen, die genauso effizient bearbeitet werden wie die vorhandenen Typen. Auch darauf werde ich später noch zurückkommen.
Vermutlich wird es Sie auch freuen, dass sich C# an den moderneren Methoden der Fehlersuche orientiert. Die traditionelle Methode, ein C++-Programm zu entwanzen, besteht darin, den Quelltext mit eingestreuten #ifdefs zu überschwemmen und umfangreiche Codeteile zu schreiben, die nur zur Fehlersuche gebraucht werden. Letztlich erhält man dadurch zwei Versionen desselben Programms, nämlich eine Debug-Version und eine Lieferversion. Manche Aufrufe in der Lieferversion landen dann in irgendwelchen Platzhaltern, die rein gar nichts zu tun haben. C# bietet Ihnen mit dem Schlüsselwort conditional die Möglichkeit, den Programmfluss anhand von definierten Symbolen zu steuern.
Erinnern Sie sich noch an den Namensraum SystemJournal? Eine einzige conditional-Anweisung reicht aus, um die Funktion SayHi zu einer Funktion zu machen, die nur bei der Fehlersuche eine Rolle spielt.

using System; 
namespace SystemJournal 
{ 
    public class HelloWorld 
    { 
        [conditional("DEBUG")] 
        public static void SayHi() 
        { 
            Console.WriteLine("Hello, World!"); 
            return; 
        } 
    ... 
    } 
} 

Konditionale Funktionen müssen übrigens den Ergebnistyp void haben, wie in diesem Beispiel gezeigt (eigentlich sind es daher keine Funktionen mehr, sondern Prozeduren.) Um die HelloWorld-Nachricht auf den Bildschirm zu zaubern, müsste das aufrufende Programm so aussehen:

using System 
using SystemJournal 
#define DEBUG 
class CallingSystemJournal 
{ 
    public static void Main(string[] args)  
    { 
        HelloWorld.SayHi(); 
        return 0; 
    } 
} 

Dieser Code ist schön übersichtlich und wird nicht durch die vielen ifdefs aufgeblasen, die doch nur zwischen den eigentlichen Codezeilen herumlungern und intensivst darauf warten, ignoriert zu werden.
Auß;erdem wurde C# so konzipiert, dass sich die Sprache leicht parsen lässt. Es sollte also nicht übermäß;ig schwer sein, entsprechende Werkzeuge zu bauen, mit denen sich der Quelltext bewältigen und in lauffähigen Code umwandeln lässt.

Objektorientierung

C++ ist objektorientiert. Richtig. Ich habe selbst Leute kennen gelernt, die sich für eine Woche oder so mit der Mehrfachvererbung herumgeschlagen haben und dann irgendwohin verschwunden sind, um verseuchte Lagunen von den Abfällen der modernen Zivilisation zu reinigen. Deswegen lässt C# die Mehrfachvererbung sausen und hält sich lieber an das virtuelle Objektsystem von .NET. Kapselung, Vielgestaltigkeit (Polymorphie) und Vererbung bleiben erhalten, die Magenkrämpfe aber nicht.
C# versenkt zudem das ganze Konzept der globalen Funktionen, Variablen und Konstanten im Mülleimer der Zeit. Statt dessen können Sie in den Klassen statische Datenelemente anlegen. Dadurch wird der Code übersichtlicher und weniger anfällig für Namenskonflikte.
Wo wir schon beim Thema Namenskonflikte sind - haben Sie etwa schon vergessen, wie Sie in den Klassen Datenelemente definiert und sie später im Code umdefiniert haben? C#-Methoden sind übrigens von Haus aus nicht virtuell. Virtuelle Funktionen verlangen explizit nach der Angabe virtual. Es ist daher nicht so einfach, versehentlich eine Methode zu überschreiben. Auß;erdem können Sie leichter für die korrekten Versionen sorgen und die vtable wächst nicht so schnell. Die Datenelemente lassen sich in C# als privat, geschützt, öffentlich oder intern definieren (private, protected, public, internal). Sie erhalten die totale Kontrolle über deren Kapselung.
Methoden und Operatoren lassen sich in C# überladen, allerdings mit einer Syntax, die wesentlich einfacher zu verstehen ist, als die C++-Variante. Globale Operatorfunktionen lassen sich aber nicht überschreiben. Die Überschreibungen gelten immer lokal. Die Methode F im folgenden Beispiel zeigt, wie die Überschreibungen aussehen:

interface ITest 
{ 
    void F();             // F() 
    void F(int x);        // F(int) 
    void F(ref int x);    // F(ref int) 
    void F(out int x);    // F(out int) 
    void F(int x, int y); // F(int, int) 
    int F(string s);      // F(string) 
    int F(int x);         // F(int) 
} 

Das Komponentenmodell von .NET wird durch die Implementierung von "Vertretern" (delegates) unterstützt, dem objektorientierten Gegenstück zu Funktionszeigern.
Schnittstellen beherrschen die Mehrfachvererbung. Die Klassen können durch die explizierte Implementierung von Funktionen private, interne Schnittstellen implementieren, ohne dass die Verbraucher jemals davon erfahren.

Typsicherheit

Auch wenn manche Leser nicht mit mir übereinstimmen, bin ich davon überzeugt, dass eine strengere Typsicherheit die Stabilität der Programme fördert. In C# wurde einiges von Visual Basic übernommen, was die korrekte Code-Ausführung fördert - und damit auch stabilere Programme. So werden zum Beispiel alle dynamisch angelegten Objekte und Arrays mit null initialisiert. Lokale Variablen initialisiert C# zwar nicht automatisch, aber der Compiler wird Sie darauf hinweisen, falls Sie eine lokale Variable vor ihrer Initialisierung benutzen. Und beim Zugriff auf ein Array erfolgt automatisch eine Bereichsüberprüfung. Im Gegensatz zu C und C++ können Sie in C# also nicht versehentlich auf ungültige Speicherbereiche zugreifen.
In C# können Sie keine ungültige Referenz anlegen. Alle Typkonvertierungen (Casts) müssen sicher sein und Sie können keine Konvertierungen zwischen ganzen Zahlen und Referenztypen vornehmen. Die Speicherbereinigung in C# sorgt dafür, dass keine streunenden Referenzen im Code herumhängen. Damit Hand in Hand geht die Überlaufsprüfung. Arithmetische Operationen und Konvertierungen sind nicht erlaub, wenn die Zielvariable oder das Zielobjekt überläuft. Natürlich gibt es aber gute Gründe, die dafür sprechen, eine Variable überlaufen zu lassen. Sollte solch ein Fall vorliegen, können Sie die Überlaufsprüfung explizit ausschalten.
Wie schon erwähnt, sind die Datentypen in C# etwas anders, als Sie es von C++ her gewohnt sind. So ist der Typ char zum Beispiel 16 Bit breit. Auß;erdem gibt es von Haus aus einige nützliche zusätzliche Typen wie decimal und string. Der wohl größ;te Unterschied zwischen C++ und C# zeigt sich aber in der Art und Weise, wie C# mit Arrays umgeht.
Arrays sind in C# "verwaltete Typen" (managed types). Das bedeutet, dass sie Referenzen enthalten, also keine Werte, und dass sich bei Bedarf der Müllsammler um sie kümmert. Sie können Arrays auf verschiedene Weisen deklarieren, auch als mehrdimensionale (rechteckige) Arrays und als Arrays von Arrays. Beachten Sie bitte im folgenden Beispiel, dass die rechteckigen Klammern hinter dem Typ stehen, also nicht hinter dem Namen, wie in manchen anderen Sprachen.

int[ ] intArray;               // ein einfaches Array 
int[ , , ] intArray;           // ein mehrdimensionales Array mit dem 
                               // Rang 3 (3 Dimensionen) 
int[ ][ ] intArray             // ein Array von Arrays 
int[ ][ , , ][ , ] intArray;   // ein eindimensionales Array, das  
                               // dreidimensionale Arrays aus zwei- 
                               // dimensionalen Arrays enthält 

Auch Arrays sind Objekte. Bei ihrer ersten Deklaration haben sie noch keine Größ;e. Daher müssen sie nach ihrer Deklaration erst einmal angelegt werden. Nehmen wir an, Sie bräuchten ein Array mit 5 Elementen. Der folgende Code liefert es:

int[] intArray = new int[5]; 

Falls Sie das zweimal hintereinander tun, wird das Array automatisch mit der jeweils aktuellen Größ;e angelegt. Aus den Zeilen

int[] intArray; 
intArray = new int[5]; 
intArray = new int[10]; 

resultiert also ein Array namens intArray, das 10 Elemente enthält. Ähnlich simpel gestaltet sich die Erzeugung eines rechteckigen Arrays:

int[] intArray = new int[3,4]; 

Etwas schwieriger ist dagegen die Erstellung eines Arrays aus Arrays ("jagged", also "unregelmäß;ige" Arrays). Der Gedanke drängt sich auf, new int[3][4] sei das korrekte Zauberwort, aber die Formel ist etwas aufwendiger:

int[][] intArray = new int[3][]; 
For (int a = 0; a < intArray.Length; a++) { 
    intArray[a] = new intArray[4]; 
} 

Mit Hilfe der geschweiften Klammern können Sie ein Array in derselben Zeile initialisieren, auf der Sie es anlegen:

int[] intArray = new int[5] {1, 2, 3, 4, 5}; 

Das gilt auch für ein String-Array:

string[] strArray = new string[2] {"MIND", "System Journal"}; 

Wie erwartet, ist der klammertechnische Aufwand bei der Initialisierung eines mehrdimensionalen Arrays etwas höher:

int[,] intArray = new int[3, 2] { {1, 2}, {3, 4}, {5, 6} }; 

Natürlich lassen sich auch die unregelmäß;igen Arrays initialisieren:

int[][] intArray = new int[][] { new int[] {2,3,4},  
                                 new int[] {5,6,7} }; 

Wenn Sie den new-Operator weglassen, ergibt sich aus der Initialisierung des Arrays sogar implizit dessen Dimension:

int[] intArray = {1, 2, 3, 4, 5}; 

Arrays werden in C# als Objekte angesehen und daher auch wie Objekte behandelt, also nicht als adressierbare Bytefolge. Insbesondere unterliegen auch Arrays der automatischen Speicherbereinigung und müssen daher nach Gebrauch nicht explizit entsorgt werden. Arrays beruhen auf der C#-Klasse System.Array. Vom Konzept her könnten Sie Arrays also als ein Sammlungsobjekt (Collection) ansehen und mit Hilfe des Length-Attributs über jedes Arrayelement gehen. Wenn Sie intArray so definieren, wie oben gezeigt, wird der Aufruf

intArray.Length 

die Länge 5 ergeben. In der Klasse System.Array gibt es auch Funktionen zum Kopieren und Sortieren der Arrays und zur Suche nach bestimmten Elementen.
C# bietet einen foreach-Operator an, der wie sein Gegenstück in Visual Basic funktioniert und den Gang über das Array ermöglicht. Werfen Sie einen Blick auf die folgenden Zeilen:

int[] intArray = {2, 4, 6, 8, 10, -2, -3, -4, 8}; 
foreach (int i in intArray) 
{ 
   System.Console.WriteLine(i); 
} 

Dieser Code zeigt die Zahlen aus dem intArray auf der Systemkonsole an, wobei jede neue Zahl auf ihrer eigenen Zeile steht. Mit der GetLength-Funktion aus der Klasse System.Array könne man diesen Code auch so formulieren (die Array-Elemente werden in C# von null an gezählt):

for (int i = 0; i < intArray.GetLength(); i++)  
{ 
    System.Console.WriteLine(i); 
} 

Skalierbarkeit

C und C++ erfordern alle möglichen Arten von oftmals inkompatiblen Header-Dateien, damit man auch nur den einfachsten Code kompilieren kann. C# kombiniert die Deklaration und Definition der Typen und macht die immer umfangreicheren Header damit überflüssig. Auß;erdem importiert und schreibt es .NET-Metadaten, so dass ein inkrementelles Kompilieren der Anwendungen wesentlich einfacher ist.
Ist ein Projekt hinreichend gewachsen, können Sie den Code auch über mehrere kleinere Dateien verteilen. C# macht Ihnen keine Vorschriften darüber, wo die Quelltexte zu liegen haben oder wie die Dateien heiß;en müssen. Wenn Sie ein umfangreiches C#-Projekt kompilieren, können Sie sich den Vorgang so vorstellen, dass der Compiler alle Quelltextdateien aneinander hängt und dann in eine groß;e Ergebnisdatei kompiliert. Sie brauchen sich nicht darum zu kümmern, welche Header-Dateien wohin gehören oder welche Funktionen in welcher Quelldatei zu liegen haben. Das bedeutet auch, dass Sie Ihre Quelltexte quasi nach Belieben verlegen, umbenennen, aufteilen oder zusammenfassen können, ohne den Compiler ernsthaft zu behindern.

Versionsunterstützung

Die DLL-Hölle ist ein ständig lästiger werdendes Problem, und zwar nicht nur für die Entwickler, sondern auch für die Anwender. Im MSDN-Online findet sich sogar ein spezieller Dienst für die Anwender, die sich mit den verschiedenen Versionen der System-DLLs herumschlagen müssen. Nun gibt es leider nichts, was eine Programmiersprache tun kann, um den Entwickler einer Bibliothek davon abzuhalten, mit einer veröffentlichten API-Schnittstelle in seine eigene Vorstellungswelt abzudriften. Aber C# wurde schon so ausgelegt, dass die Versionsverwaltung wesentlich einfacher wird, weil die Binärkompatibilität zu den vorhandenen abgeleiteten Klassen erhalten bleibt. Wenn Sie in einer Basisklasse eine neue Funktion einführen, die es bereits in der abgeleiteten Klasse gibt, resultiert daraus kein Fehler. Allerdings muss der Entwickler der Klasse kenntlich machen, ob die Methode als Überschreibung gedacht ist oder als neue Methode, die einfach nur die entsprechende geerbte Methode verdeckt.

Kompatibilität

Auf den Windows-Plattformen haben sich im Lauf der Zeit einige verschiedene API-Formen ausgebildet und C# kommt mit allen zurecht. Die alten Schnittstellen nach C-Art lassen sich direkt in C# ansprechen. C# bietet den transparenten Zugriff auf die API-Standardfunktionen von COM und OLE-Automation und unterstützt alle Datentypen aus der .NET-Laufzeitschicht. Und wichtiger noch, C# beherrscht auch die Common Language Subset-Spezifikation von .NET. Wenn Sie irgendwelche Objekte exportiert haben, die von anderen Sprachen aus nicht zugänglich sind, kann der Compiler den Code bei Bedarf entsprechend kennzeichnen. So kann eine Klasse zum Beispiel nicht die Funktionen runJob und runjob exportieren, weil eine Programmiersprache, die nicht zwischen Groß;- und Kleinschreibung unterscheidet, damit überfordert wäre.
Wenn Sie eine Funktion aufrufen, die von einer DLL exportiert wird, müssen Sie die Funktion deklarieren, ihr ein sysimport-Attribut geben und bei Bedarf die Angaben über das anwenderdefinierte Marshaling und den Ergebnistyp machen, die von den .NET-Vorgaben abweichen.
Die folgenden Zeilen zeigen ein kleines HelloWorld-Programm, das seinen üblichen Jubelruf in einem der üblichen Nachrichtenfenster von Windows in die Welt hinaustrompetet:

class HelloWorld 
{ 
    [sysimport(dll = "user32.dll")] 
    public static extern int MessageBoxA(int h, string m,  
                                         string c, int type); 
    public static int Main()  
    { 
        return MessageBoxA(0, "Hello World!", "Caption", 0); 
    } 
} 

Jeder .NET-Typ lässt sich auf einen passenden Typ abbilden, den .NET zur Weiterleitung des Werts über einen API-Aufruf benutzt. Der C#-Typ string wird zum Beispiel auf LPSTR abgebildet. Aber das lässt sich mit marshaling-Anweisungen ändern:

using System; 
using System.Interop; 
class HelloWorld 
{ 
    [dllimport("user32.dll")] 
    public static extern int MessageBoxW(  
        int h,  
        [marshal(UnmanagedType.LPWStr)] string m,  
        [marshal(UnmanagedType.LPWStr)] string c,  
        int type); 
    public static int Main()  
    { 
        return MessageBoxW(0, "Hello World!", "Caption", 0); 
    } 
} 

Sie können aber nicht nur mit DLL-Exporten arbeiten, sondern auch mit den klassischen COM-Objekten, und zwar auf recht klassischem Wege: man legt die Objekte mit CreateInstance an, fordert die gewünschten Schnittstellen an und ruft deren Methoden auf.
Der Import einer COM-Klassendefinition ins Programm erfolgt in zwei Schritten. Zuerst müssen Sie eine Klasse anlegen und ihr das Attribut comimport geben, um es mit einer bestimmten GUID zu verknüpfen. Diese Klasse, die Sie hier anlegen, kann weder irgendwelche Basisklassen oder Schnittstellenlisten haben noch irgendwelche Elemente.

// deklariere FilgraphManager als eine COM-Coklasse 
[comimport, guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]  
class FilgraphManager  
{  
} 

Sobald die Klasse in Ihrem Programm deklariert ist, können Sie mit dem Schlüsselwort new, das in diesem Fall mit der Funktion CoCreateInstance äquivalent ist, eine neue Instanz der Klasse anlegen.

class MainClass  
{  
    public static void Main()  
    {  
        FilgraphManager f = new FilgraphManager();  
    }  
} 

Sie können die Schnittstellen in C# indirekt anfordern, indem Sie versuchen, das Objekt per Cast in die gewünschte Schnittstelle zu konvertieren. Sollte diese Typumwandlung fehlschlagen, wird eine System.InvalidCastException ausgelöst. Klappt die Umwandlung aber, steht Ihnen ein Objekt zur Verfügung, das diese Schnittstelle repräsentiert.

FilgraphManager graphManager = new FilgraphManager(); 
IMediaControl mc = (IMediaControl) graphManager; 
mc.Run(); // Diese Zeile funktioniert, sofern die Typumwandlung  
          // geklappt hat. 

Flexibilität

Es stimmt schon, dass C# und .NET eine verwaltete, typsichere Umgebung bilden. Allerdings stimmt es auch, dass manche Anwendung in der realen Welt bis zur Ebene der Assemblersprache hinabsteigen muss, sei es aus reinen Leistungsgründen oder zur Anpassung an irgendwelche seltsamen Schnittstellen aus anderen Programmen. Bisher habe ich nur kurz skizziert, wie man von C# aus API-Funktionen und COM-Komponenten benutzt. In C# ist es aber auch möglich, unsichere Klassen und Methoden zu deklarieren, die Zeiger, structs und statische Arrays enthalten. Diese Methoden sind zwar nicht typsicher, aber sie laufen trotzdem im verwalteten Raum, so dass es keine künstlichen Hürden zwischen sicherem und unsicherem Code gibt, die nur per Marshaling zu überwinden wären.
Diese unsicheren Kandidaten lassen sich relativ zwanglos betreiben. Der Entwickler kann ein Objekt entsprechend kennzeichnen, so dass die Speicherbereinigung es einfach ignoriert, wenn sie ihre Runden dreht. Der unsichere Code wird nicht auß;erhalb der vertrauenswürdigen Umgebung ausgeführt. Der Programmierer kann die Speicherbereinigung sogar vollständig ausschalten, solange eine unsichere Methode läuft.