Freigeben über


Die Sprache C# im Detail

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

Von Marcellus Buchheit

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

Bild01

C# (sprich C sharp) ist die Systemprogrammiersprache von .NET. Vieles was .NET an Neuem bringt, kann man direkt aus den Eigenschaften dieser Sprache ablesen. Im folgenden ein Einstieg für C++-Programmierer, die einen Umstieg auf C# planen.

C# erlebt zur Zeit in der Microsoft-Entwicklerwelt einen ähnlichen "Hype" wie vor wenigen Jahren Java auß;erhalb dieser Welt. Viele sehen C# als Nachfolger von C++ an, ohne sich näher mit der Sprache, ihren Möglichkeiten und Einschränkungen oder mit ihrer Anwendung beschäftigt zu haben. Ich möchte daher bereits am Anfang meine persönliche Bewertung der Sprache abgeben, damit dem Leser klar ist, um was es bei C# im Vergleich zu C++ eigentlich geht.
Ein wichtiger Hinweis vorab: Meine Beschreibung von C# beruht auf der .NET-Vorversion, die auf der PDC (Professional Developer Conference) im Orlando im Juli 2000 vorgestellt und verteilt wurde. Diese Vorversion hat noch nicht "Beta-Stadium", und es kann sein, dass bis zur endgültigen Standardisierung und Auslieferung von C# noch einiges an der Sprache geändert wird.

Brauchen wir schon wieder eine neue Programmiersprache?

Diese Frage werden sich zumindest C++-Programmierer stellen, die schon vor Jahren mit Java verunsichert wurden. Java wurde damals eingeführt, um Komponenten für Webseiten schreiben zu können, später sollte damit auch für die Entwicklung komplexer Anwendungen (etwa Office-Produkte) C++ und sogar das gesamte Windows-System abgelöst werden - was mittlerweile als gescheitert angesehen kann. Versucht jetzt Microsoft erneut die Ablösung von C++ mit C#?
Die deutliche Antwort ist "Nein", auch wenn einige C#-Freaks das anders sehen. C# ist eine Ergänzung zu C++, keine direkt C++ ersetzende Programmiersprache. Dazu fehlen C# ein paar wichtige Eigenschaften, doch dazu später.
Der einzige Grund für die Einführung von C# ist .NET: Dies ist eine neue Entwicklungsplattform, die sowohl das bisherige Komponentenmodell COM/OLE/DCOM ersetzt, als auch das Win32-API für Systemaufrufe ablöst (dazu mehr in anderen Artikeln in diesem Heft).
.NET standardisiert alle Basistypen für Komponenten und Systemaufrufe in einem Laufzeitsystem mit Speicherverwaltung und Ausnahmebehandlung. Die Veränderungen gehen so weit, dass herkömmliche Programmsprachen etwas erweitert werden müssen um für .NET tauglich zu sein. Bei Visual Basic ist das kein Problem, aber C++ weicht derart von den .NET-Prinzipien ab, dass es eher als exotische .NET-Sprache angesehen werden kann.
Zwar wird C++ von Microsoft für .NET voll unterstützt, aber einerseits musste die Sprache für .NET erweitert werden, andererseits können wesentliche Sprachelemente wie Zeiger oder Mehrfachvererbung in .NET-Komponentenschnittstellen nicht verwendet werden.
Java wäre ein Kandidat für solch eine Programmiersprache gewesen, aber Sun sieht natürlich .NET als Konkurrenz an und hätte daher jede erforderliche und sinnvolle Erweiterung der Sprache für .NET mit juristischen Mitteln verhindert oder verzögert, so dass Java keine Lösung für Microsoft darstellte. (Es wird jedoch von anderen Herstellern möglicherweise Java-Implementierungen für .NET geben. Rational hat zum Beispiel eine solche Implementierung angekündigt. Die Redaktion.)
So musste eine neue grundlegende Systemprogrammiersprache definiert werden, die .NET mit allen seinen Möglichkeiten und Einschränkungen exakt abbildet: Vieles, was Sie im folgenden über Typen, Speicherverwaltung, Komponentendefinition usw. erfahren, gibt es auf ähnliche Weise auch in anderen .NET-Sprachen inklusive C++.

Die Entwicklung von C#

Im C#-Team hat ein besonders bekannter Programmiersprachen-Designer unserer Tage mitgearbeitet: Anders Heijlsberg. Er hat für Borland vor etlichen Jahren Delphi entwickelt. Wer die Entwicklung von Delphi verfolgt hat, weiß; dass es sehr auf die Praxis zugeschnitten war. Diese Erfahrung hat Heijlsberg auch in C# übertragen und es ist aus diesem Grund eine sehr praxistaugliche Sprache entstanden.
Microsoft hat die Sprache darüber hinaus freigebeben, so dass sie vielleicht auch auß;erhalb der .NET-Plattform, etwa zu Programmierung von Unix-Programmen, Verwendung finden kann. Verfolgt man die Weiterentwicklung von Microsoft-Basic vor 25 Jahren bis zum Visual Basic von heute, weiß; man auch, dass Microsoft neue Anforderungen an eine Programmiersprache zeitnah als deren neue Eigenschaften festlegt, ohne dass diese jahrelang durch Standardisierungskomitees verzögert werden. Bei C# ist das nicht anders zu erwarten, ein weiterer Vorteil für die Unabhängigkeit von C# gegenüber C++ oder Java.

Ist C# der Nachfolger von C++?

Wie Sie im Verlauf des Artikels noch sehen werden ist C# eine Programmiersprache, mit der man hervorragend kleine und mittelgroß;e Komponenten schreiben kann oder ähnliche groß;e Konsole- und GUI-Anwendungen zum Aufrufen solcher Komponenten. Wer solche Programme bisher in C++ schreibt und trotz der eleganten .NET-Erweiterungen im zukünftigen C++ eine einfacherere aber ähnliche Sprache sucht, ist mit C# gut bedient.
Wer dagegen sehr groß;e Anwendungen oder Komponenten schreibt, die als einzelne EXE- oder DLL-Datei ausgeliefert und funktionieren soll, für den wird C# vermutlich nicht die richtige Wahl darstellen: Wie wir noch sehen werden, erlaubt C# weder die Erzeugung von OBJ-Dateien noch die Einbindung von LIB-Bibliotheken, der Compiler baut immer mit einem Aufruf eine oder mehrere C#-Quelldateien zu einer einzigen EXE- oder DLL-Datei zusammen.
Nicht nur der Compiler, auch die Sprache selbst, ist nicht für die Entwicklung oder Benutzung statischer Bibliotheken gedacht: Es gibt nicht wie C++ die Trennung von Deklaration (in .H-Dateien) und Definitionen (in .CPP-Dateien), wie bei C++-Inline-Code steht beides zusammen und muss dann auch gleichzeitig übersetzt werden.
Microsoft wird also kaum in Zukunft die Anwendung Word in C# statt in C++ schreiben können, auß;er es wird aus der heutigen EXE-Datei mit mehr als 8 MByte eine kleine Anwendung und sehr vielen kleinen Komponenten gemacht: Für groß;e Projekte stellt C# gegenüber C++ einen Übergang vom statischen zum dynamischen Linken dar. Wenn Sie immer schon der Meinung waren, dass dynamisch hinzugebundene Komponenten gegenüber Bibliotheken das Non-Plus-Ultra sind, dann liegen Sie mit einem Wechsel auf C# möglicherweise richtig, wenn Sie dagegen bei wenigen groß;en Programmmodulen bleiben wollen, die aus Bibliotheken und groß;en makefile-Dateien vom Linker zusammengesetzt werden, dann bleiben Sie bei C++.
Auch das Umschreiben von C++- in C#-Code ist nicht so einfach, wie es auf den ersten Blick vielleicht aussieht. Nicht nur die Aufhebung der Trennung von Deklaration und Definition macht Probleme, auch bereits einfachste Befehle erfahrener C++-Progammierer. Ein Beispiel ist die C++-Dualität von Zeigern und Feldern. Viele C++-Programmierer (inklusive mir) schreiben statt

int an[20]; 
for (int i = 0; i < 10; i++) { 
  an[i] = i; 
  an[i+10] = an[i] * an[i]; 
} // for 

lieber:

int an[20]; 
int *pn = an; 
for (int i = 0; i < 10; i++, pn++) { 
  *pn = i; 
  pn[10] = *pn * *pn; 
} // for 

In C# kann man aber nicht so einfach Zeiger auf Felder setzen wie bei C++ so dass viele C++-Programme aufwendig umgeschrieben werden müssen und das C#-Resultat sorgfältig getestet werden muss. Zumindest ist eine automatische Umsetzung von C++ in C#-Programme faktisch unmöglich.
So werden wir sicherlich noch lange zusätzlich zu C# auch noch mit C++-Neu- und Weiterentwicklungen zu tun haben. Am besten sehen Sie als erfahrenerer C++-Programmierer C# nicht als Konkurrenz, sondern als Ergänzung an und nutzen die neuen Möglichkeiten im Rahmen von .NET vor allem dort, wo C++ seine Vorteile nicht ausspielen kann und wo eh alles neu entwickelt werden muss.
Für noch eine Entwicklergruppe dürfte C# hochinteressant sein: Wenn Sie bisher von der Entwicklungsumgebung von Visual Basic recht angetan waren, etwa mit der einfachen Verknüpfung von GUI-Dialogen und Funktionen, nur die Sprache mit dem vielen Text und den fehlenden Strichpunkten am Ende der Statements nicht mochten. C# wird eine weitgehend mit Visual Basic identische Entwicklungsumgebung haben, in der GUI-Anwendungen ohne Ressourcendateien entwickelt werden und die Eigenschaften von Dialogelementen viel leichter im Code abgefragt und verändert werden können, als dies bei C++ heute auß;erhalb von .NET möglich ist. Ob diese Entwicklungsumgebung in Zukunft auch bei C++ die gleichen Leistungen vollbringen kann, ist zumindest zum Zeitpunkt der .NET-Vorversion nicht klar.

Ein erstes Beispiel

Wenn Sie noch an C# interessiert sind, dann geht's jetzt richtig zur Sache. Vorweg zeige ich Ihnen ein kleines Beispiel, das ihnen aus der C/C++-Welt sicherlich bekannt vorkommen wird: "Hello World". Ein solches Programm sieht in C# wie folgt aus:

/* Das HelloWorld-Beispiel für das System Journal 
 */ 
using System; 
class HelloSystemJournal 
{ 
  public static void Main() 
  { 
    Console.WriteLine("C# grüß;t das System Journal"); 
  } // Main() 
} // HelloSystemJournal 

Was ist anders als bei C++? Es gibt eine Hauptklasse HelloSystemJournal, deren Methode Mainbeim Start der Anwendung ausgeführt wird. C# kennt keine Funktionen oder Variablen auß;erhalb von Klassen: Alles wird innerhalb einer Klasse definiert. Die Ausgabe des Texts erfolgt über das Objekt Console und dessen Methode WriteLine, beide sind in der Klasse System definiert, die in der ersten Zeile mit using eingebunden werden. Diese Definitionen stammen von der .NET-Systemumgebung, die das Win32-API ersetzt.
Sonst sieht ein C#-Programm durchaus einem C++-Programm recht ähnlich, etwa was Kommentare, Zeichenfolgen, Blöcke oder Namen wie class betrifft. Die Unterschiede sind im Detail aber beträchtlich und ich gehe im folgenden ausführlich darauf ein.

Aufruf des Compilers

Im Gegensatz zu Visual Basic kann man C# auch mit einem beliebigen Texteditor entwickeln. Der Compiler heiß;t CSC und wenn das HelloWorld-Beispiel in der Textdatei Hello.CS abgelegt ist, dann erzeugt der Aufruf von

csc hello.cs 

in Sekundenschnelle eine Konsolenanwendung in einer einzelnen EXE-Datei, die beim Aufruf mit installierter .NET-Laufzeitumgebung den gewünschten Text am Bildschirm anzeigt.
Der Compiler besitzt eine Reihe von Optionen, die man mit dem Aufruf von csc ohne Argument angezeigt bekommt. Einige wichtige Optionen habe ich in Tabelle T1 angegeben, man muss die Optionen ähnlich wie beim Visual Studio 6.0-Linker ausschreiben oder kann sie teilweise abkürzen.
Auf einige der Optionen kommen wir im weiteren Teil des Artikels noch zu sprechen.

T1 Die wichtigsten Optionen des C#-Compilers CSC.EXE

Option

Bedeutung

/target:XX
oder
/t:XX

Die wichtigste Option überhaupt, mit der in XX festgelegt wird, ob man eine Konsoleanwendung (exe), eine GUI-Anwendung (winexe), eine eigenständige DLL-Komponente (library) oder eine DLL-Teilkomponente eines Assembly (module) erzeugen möchte.

/win32icon:file

Bindet eine Icon-Datei in die erzeugte Datei ein, zur Anzeige in der Windows-Shell.

/reference:file
oder
/r:file

Bindet Metadata (= Importinformation) aus dem angegebenen Assembly ein, die für die C#-Übersetzung benötigt wird.

/resource:file
oder
/res:file

Bindet NGWS-Resourceninformation aus der angegebenen Datei in das erzeugte Modul ein.

/linkresource:file
oder
/linkres:file

Bindet einen Verweis auf die NGWS-Resourceninformation aus der angegebenen Datei in das erzeugte Modul ein (die Resourcendatei selbst muss mitgeliefert werden).

/win32res:file

Erlaubt die Übernahme von Win32-Ressourceninformation aus einer übersetzten .RES-Datei. Diese wird verwendet für Versionsangaben, Bitmaps, Icons oder Cursors, aber nicht für Dialoge.

/define:XX
oder
/d:XX

Definiert eine oder mehrere Präprozessorvariablen als vorhanden. Man kann ihnen aber keine Werte zuweisen.

/checked:+|-

Aktiviert oder deaktiviert die Laufzeitprüfung für arithmetischen Überlauf

/unsafe:+|-

Aktiviert oder deaktiviert die Erzeugung von Safe oder Unsafe Code. "Unsafe" Code kennt unsaubere, pointer-ähnliche Konvertierungen, "Safe" ist der Code für .NET.

/debug:+|-

Erzeugt Debugging-Informationen im Modul oder nicht.

/optimize:+|-

Erzeugt optimierten Code oder nicht.

/incremental:+|-

Erlaubt inkrementelle Kompilierung zur Erhöhung der Übersetzungsgeschwindigkeit.

/warn:level

Setzt den Level der Warnungsausgabe bei unsauberen Angaben im Quellcode.

/doc:file

Erzeugt eine XML-Dokumentdatei aus dem Quellcode, die von Visual Studio für Referenzen, Querverweise usw. ausgewertet werden kann.

Der Präprozessor für bedingte Compilierung

Für C++-Programmierer ist der Präprozessor ein wichtiger Teil der Sprache, häufig wird er verwendet, um die Sprache zu erweitern, beispielsweise mit Makros, die COM/OLE-Komponentenaufrufe erleichtern für oder die Definition von Nachrichtentabellen in der MFC.
C# kennt keine Makros im Präprozessor, dementsprechend kann auch die Sprache nicht verändert werden. C# erlaubt aber mit den bekannten Präprozessorbefehlen #if, #else, #elif und #endif die bedingte Compilierung, die man etwa bei Java schmerzlich vermisst. Mit #define und #undef kann man hierfür Namen definieren oder ihre Definition aufheben, man kann den Namen aber keine Werte zuweisen. Nach #if beispielsweise können auch logische Ausdrücke stehen, sie prüfen aber nur, ob ein Name definiert ist oder nicht, beispielsweise:

#define YES 
#undef NO 
#if YES && !NO 
  ... 
#endif 

Konstanten kann man also mit dem Präprozessor nicht festlegen, noch kann man mit #define festgelegte Namen auß;erhalb der Präprozessorzeilen im eigentlichen C#-Quellcode verwenden.
Wie bei C++ kann man aber #define-Angaben in das Argument des Compileraufrufs verlagern, wozu die Option /define oder /d verwendet wird.
Wenn ein Entwickler sein bestehendes C++-Programm einfach mal durch den C#-Compiler jagt, wird er meistens als erstes über die bei C# nicht definierte #include-Anweisung stolpern und sich erst mal fragen, wie er die denn übertragen soll. Am fehlenden #include, fehlenden Header-Dateien und der Aufhebung zwischen Definition und Deklaration scheiden sich bei C++ und C# endgültig die Geister und es beginnt bei C# die Welt der Komponentenprogrammierung mit Metadata, Assemblies usw. Dazu später mehr.
Neu gegenüber C++ ist der Präprozessorbefehl #warning, der ähnlich wie #error die Ausgabe einer Warnung zusätzlich zu den vom Compiler erzeugten Fehler- und Warnungsausgaben erlaubt.

Es kann auch eleganter aussehen ...

Die bedingte Compilierung à la C/C++ ist sehr nützlich, um Teile von Definitionen oder Ausführungen abhängig von äuß;eren Bedingungen zu definieren. Für die Definition ganzer Funktionen abhängig von Bedingungen gibt es ein Sprachkonstrukt in eckigen Klammern mit dem Schlüsselwort conditional:

[conditional ("PräprozessorSymbol")] 
FunctionDefinition 

Die Bedeutung dieses Konstrukts ist ganz einfach: Wenn das Symbol definiert ist, wird die folgende Funktionsdefinition angenommen. Ist von den angegebenen Symbolen keines definiert, werden alle Funktionsaufrufe automatisch im Code weggelassen.
Damit kann man auf einfache Weise zum Beispiel eine Trace-Ausgabefunktion erzeugen, die nur bei der Definition der Variable DEBUGCON oder DEBUGFILE aktiviert wird und dann unterschiedliche Ausgabestellen verwendet.

Einfache Typen

Bei den Typen stoß;en wir das erstemal auf die .NET-Plattform: Alle vordefinierten C#-Typen entstammen aus dieser Umgebung und finden sich beispielsweise auch bei Visual Basic oder .NET-C++ wieder. Tabelle T2 gibt einen Überblick. C++-Programmierern fällt die 64-Bit-Definition von long und ulong auf - auch solche Dinge erschweren die Portierung von C++-Quellcode auf C# nicht unwesentlich. Alle Arithmetikroutinen mit diesen Typen sind von .NET definiert und damit bei allen Programmiersprachen exakt gleich. So steht auch der Decimal-Typ endlich auch auß;erhalb von Visual Basic allen Programmierern offen, Konstanten hierfür werden mit einem abschließ;enden "m" angegeben, zum Beispiel

decimal dcSample = 195.35m; 

Der bool-Typ wurde gegenüber C++ noch stärker von Integer abgegrenzt: Konvertierung zwischen solchen Basistypen sind nur über Casts möglich, das gleiche gilt für die Konvertierung zwischen Integer und Fließ;komma oder decimal.
C++-Programmierer müssen sich auch daran gewöhnen, dass char nicht mehr länger das Gleiche wie byte ist, sondern jetzt ein Unicode-Zeichen mit 2 Bytes verwaltet. Zeichenkonstanten werden dagegen auch bei C# mit einfachen Apostrophen angegeben, die üblichen \-Sonderzeichen sind auch weiterhin möglich etwa

char chNewLine = '\n' 

Neu ist \u zur Angabe von Unicode-Konstanten im Hexformat. Auch zwischen char und Integer gibt es keine automatischen Konvertierungen mehr, (char)65 oder (int)'A' sind aber weiterhin möglich.

T2 Die Basisypen von C# (und .NET)

Name

Bedeutung

sbyte

Integerwert zwischen -128 und +127

byte

Integerwert zwischen 0 und +255

short

Integerwert zwischen -32768 +32767

ushort

Integerwert zwischen 0 und +65535

int

Integerwert zwischen -231 +231-1

uint

Integerwert zwischen 0 und +232-1

long

Integerwert zwischen -263 und 263-1

ulong

Integerwert zwischen 0 und +264-1

bool

Boolescher Wert, kann nur true oder false annehmen.

char

Ein einzelnes Unicode-Zeichen

float

Ein 32-Bit-Fließ;kommawert mit 7 Stellen Genauigkeit

double

Ein 64-Bit-Fließ;kommawert mit 15 Stellen Genauigkeit

decimal

Ein 128-Bit-Wert mit 28 bis 29 Stellen Genauigkeit im Dezimalsystem für präzise Geldberechnungen

Der Aufzählungstyp

Bei den zusammengesetzten Typen gibt es zunächst enum zur Definition von Aufzählungskonstanten, vom Prinzip her erst mal aus C++ übernommen. Neu ist aber die optionale Angabe der Breite des Basistyps, hier kann man byte/sbyte (8 Bit), short/ushort (16 Bit) int/uint (32 Bit, int Standard) oder long/ulong (64 Bit) angeben, beispielsweise:

enum MonthLength : byte { 
  January = 31, 
  February = 28 
}; 
MonthLength eml = MonthLength.February; 

Dieses Feature habe ich bei C++ schon lange vermisst, um Aufzählungswerte für Binärformate festlegen zu können. Leider sind im Zeitalter von XML Binärdateien und Binärübertragungen auß;er Mode, so dass dieses Feature kaum mehr sinnvolle Anwendung finden dürfte, auß;er zum Sparen von Speicherplatz in häufig verwendeten Strukturen.
Ähnlich wie bei char erlaubt C# auch bei enum keine Konvertierung zu Integer auß;er über einen Cast; dafür ist enum jetzt aber ein eigener Datentyp, der auch die Operationen ++ und -- unterstützt - in C++ ist das alles nicht so sauber festgelegt.

Von fehlenden Zeigern zu neuartigen Referenzen

Den klassischen C++-Zeigertyp, den eigentlich nur ein Assemblerprogrammierer richtig versteht, indem er weiß;, dass er auf eine Adresse im Daten- oder Codebereich zeigt, vergessen Sie am besten gleich, wenn sie mit C# programmieren. Statt dessen gibt es jetzt Referenzen, die man auch von der COM-Programmierung und Visual Basic kennt.
Referenzierte Objekte belegen immer dynamischen Speicher und verwalten automatisch ihre Benutzung. Werden Sie nicht mehr benutzt, wird der Speicher automatisch bereinigt (Garbage Collection). Es gibt zwei grundlegende Referenztypen:

  • object wird von .NET festgelegt und ist der Basistyp für alle referenzierten Objekte.

  • class dient zur Definition einer Struktur, also einem Objekt aus zusammengesetzten Typen, die als Referenz verwaltet wird.

Eine Variable, die als object definiert ist, kann damit jeden Typ annehmen, der von ihr abgeleitet ist. Dennoch ist es nicht einfach ein void*-Zeiger, denn es werden damit ja noch Referenzen automatisch verwaltet (für C++-Programmierer mit ATL-Erfahrung erinnert das ganze eher an Smart Pointer). Einer solchen Referenz kann man beispielsweise einen Integer-Wert zugewiesen:

object onValue = 123; 

Boxing und Unboxing

Manchmal hat man schon einen Wert in einer gewöhnlichen Variablen und möchte ihn in ein Referenzobjekt übertragen, um den Wert besser zwischen verschiedenen Instanzen ohne Duplikation verwalten zu können. Bei C++ setzt man hierzu einen Zeiger auf die Variable, bei C# macht man dies mit einem object:

int nValue = 123; 
object onValue = nValue; 

Doch Vorsicht mit Vergleichen: Gegenüber C++ wird bei C# der Inhalt von nValue in dynamisch verwalteten Speicher übertragen, also von der Speicherstelle nValue vollständig gelöst. Damit entfällt eine vielfache Ursache von Schutzverletzungen oder unerklärlichem Programmablauf bei C++. Diese Übertragung eines Werts in eine Referenz wird Boxing genannt.
Der umgekehrte Weg wird entsprechend Unboxing genannt: die Übertragung einer Referenz in eine einfache Variable. Hierbei muss immer ein Cast angegeben werden, damit das Laufzeitsystem überprüfen kann, ob der Objekttyp und der Zieltyp übereinstimmen:

int nValue = 123; 
object onValue = nValue; 
int nCopy = (int)onValue; 

Gegenüber void* speichert also eine Referenz auch ihren präzisen Typ; gibt es beim Unboxing keine Übereinstimmung zwischen beiden Typen, wird vom Laufzeitsystem eine Ausnahme ausgelöst (Ausnahmen werden gegen Ende des Artikels behandelt).

Zeichenfolgen sind Referenzen

Wenn Sie sich fragen, warum der elementare Typ string erst jetzt von mir ins Spiel gebracht wird und nicht schon weiter oben bei Basistypen, dann ist die Antwort einfach: Eine Zeichenfolge wird stets mit einer Referenz verwaltet und teilt damit ihren Speicherplatz ähnlich wie CString bei der MFC zwischen verschiedenen Instanzen; beim Kopieren einer Zeichenfolge werden also nicht jedes mal die Zeichen einzeln kopiert. Sonst sind die Möglichkeiten, die .NET für ihren string-Typ bietet, ähnlich wie bei C++-STL, vielleicht nicht ganz so vielseitig. Im folgenden ein paar einfache Beispiele:

string str1 = "Meine Zeichen"; 
string str2 = str1 + " mit noch mehr Text"; 
char ch = str1[3]; 

Felder sind Referenzen

Ein ähnliches Problem wie bei Zeichenfolgen stellt sich auch bei Feldern: Wenn man sie etwa bei Funktionsaufrufen übertragen möchte, dann sind Kopieroperationen bei größ;eren Feldern sehr zeitaufwendig. Deshalb wurden auch bei C# Felder als Referenzen definiert und stammen vom object-Typ ab.
Auch sonst müssen erfahrene C++-Programmierer sich bei der Benutzung von Feldern an einige Neuheiten gewöhnen, wenn sie auf C# umsteigen:

  • Feldgrenzen werden bei der Adressierung mit einem Index immer überprüft. Ein weiterer Grund mehr, warum es bei C# weder Schutzverletzungen noch uninitialisierte Variablen geben kann. Wie bei C++ beginnen aber auch Feldindizes bei C# immer bei 0.

  • Bei mehrdimensionalen Feldern gibt man die Indizes durch Komma getrennt an, ähnlich wie in Pascal und nicht mehr mit mehreren Klammern hintereinander wie bei C/C++. Die Reduktion eines mehrdimensionalen Felds durch Zeiger um eine Dimension ist eh nicht mehr möglich. Neu ist auch die offene Angabe der Dimension direkt nach dem Typ und nicht bei wie C/C++ nach dem Namen:

    int[,] aMatrix = {{1,2,3},{6,7,8},{12,13,14}}; 
    aMatrix[0, 0] = 0; 
    aMatrix[1, 1] = 1; 
    
  • Offene Größ;en von Feldern kann man jetzt auch beim dynamischen Anlegen mit dem new-Operator angeben. Dies ist natürlich unbedingt erforderlich, weil man jetzt nicht mehr ein Feld aus einem Zeiger machen kann wie bei C++:

    int cSize = 3; 
    int[] aSample = new int[cSize]; 
    int[,] aMatrix; 
    aMatrix = new int[,]{{0,0}, {1,1}, {2,2}}; 
    

    Insgesamt ähneln Felder bei C# mehr denen von Visual Basic als denen von C++. C++-Programmierer, die wissen, wie man Felder bei COM/OLE-definiert, tun sich mit den C#-Feldern auch etwas leichter.

Klassen und Strukturen

Im Gegensatz zu Java kennt C# auch Strukturen und im Gegensatz zu C++ sind Strukturen und Klassen etwas im Prinzip gleiches bei ihrer Definition, aber etwas im Prinzip völlig anderes beim Verwalten im Speicher während der Ausführung:
Klassen, definiert mit class sind Referenzen wie Felder oder Strings. Sie werden im Speicher dynamisch verwaltet, beim Kopieren wird nur die Zugriffsanzahl erhöht.
Strukturen, definiert mit struct, sind Speicherwerte, die ähnlich wie int oder float statisch im Speicher abgelegt werden, also beim Kopieren dupliziert werden.
Sonst verhalten sich Klassen und Strukturen weitgehend gleich. Beide Typen können in beliebiger Reihenfolge mit folgenden Memberwerten definiert werden:
Konstruktoren, im Prinzip wie bei C++, können optional Parameter haben; wie bei C++ gibt es auch einen Default-Konstruktor, wenn nichts angegeben ist.
Destruktoren, im Prinzip wie bei C++, werden sie bei der Freigabe des Objekts aufgerufen. Es gibt aber einen wesentlichen Unterschied: Bei Klassenvariablen wird der Destruktor nicht aufgerufen, wenn die Variable ihre Gültigkeit verliert, sondern erst viel später, wenn die Garbage-Collection Speicher freigibt.
Man sollte also in einem C#-Destruktor niemals Aktionen ausführen, die sofort ausgeführt werden müssen, etwa das Schließ;en einer Datei, die anschließ;end in einem anderen Objekt wieder geöffnet werden muss. Für solche Fälle ist es empfehlenswert, eine separate "Release"-Funktion zu definieren, die man am Ende explizit aufruft und die dann auch vom Destruktor sicherheitshalber auch noch einmal aufgerufen werden kann.
Wie bei C++ haben auch bei C# Konstruktoren und Destruktoren keinen Rückgabewert.
Methoden, im Prinzip wie bei C++. Es gibt aber, wie bereits am Anfang des Artikels erwähnt, den wesentlichen Unterschied, dass die Defintion der Methode, also ihr ausführender Code, direkt in der Klassendefinition angegeben wird. Am besten merkt man sich als C++-Programmierer, dass bei C# alles "inline" definiert ist (das Schüsselwort inline ist daher bei C# auch überflüssig und nicht reserviert):

class Sample { 
  public Sample() 
  { 
    mx = 0; 
  } // Sample() 
  int mx; 
}; // Sample 

C# erlaubt auch das Überladen von Methoden sowie von Operatoren, ähnlich wie bei C++; das Verhalten ist sehr ähnlich und ich werde darauf in diesem Artikel nicht weiter eingehen.
Fields, im Prinzip wie bei C++ werden sie zum Ablegen von Datenwerten benutzt, wobei einfache Typen, zusammengesetzte Typen oder Referenztypen angegeben werden können.
Properties, die bei C++ leider nicht vorgesehen sind, erlauben, ähnlich wie man das von COM/OLE schon seit langem kennt, die aktive Übertragung von Daten beim Lesen oder Schreiben des Members. Hierzu werden Funktionen mit den Namen set() und get() verwendet. Weiter unten kommt dazu ein Beispiel.
Indexers bauen auf Properties auf und erlauben die Benutzung eines Objekts direkt als Feld. Dies ist wichtig, damit .NET solche Grundtypen wie string definieren kann. Indexers können aber auch für eigenen Objekttypen beliebig verwendet werden. Ich gehe weiter unten in einem separaten Kapitel darauf ein.
Events ermöglichen wie in Visual Basic den Aufruf von Funktionen des Benutzers einer Klasse (ausgehender Aufruf). Ein typisches Beispiel ist die "Button"-Klasse eines Dialogs, die ein Event auslöst, wenn der Benutzer mit der Maus drauf klickt. Auch hierauf gehe ich weiter unten mit einem Beispiel ein.

Parameterübergabe bei Methoden

Gegenüber C bzw. C++ wurde die Parameterübergabe bei C# deutlich geändert. Dies ist erforderlich, da C#-Klassen direkte Verwendung als Komponentenschnittstelle finden und Komponenten auch über Prozess- und Rechnergrenzen hinweg angesprochen werden müssen. Dabei muss klar sein, welche Parameter in welche Richtung geschickt werden müssen. Wer sich als COM/OLE-Programmierer mit der IDL beschäftigt hat, findet dort ähnliche Parametertypen wie bei C#:

  • wird nichts angegeben erfolgt die Übergabe eines Parameters wie bei C/C++ als Wertkopie, genau so wie es auch die Visual-Basic-Programmierer von ByVal kennen. Zu beachten ist, dass Referenzen beim Kopieren nicht ihre Daten übertragen sondern nur die Benutzungsanzahl um 1 erhöhen. Im Gegensatz zu C++ prüft der C#-Compiler, ob eine Variable einen definierten Wert hat, wenn sie als solch ein Parameter übergeben wird.

  • Die Angabe ref entspricht dem kryptischen & bei C++: Hier wird der Wert sowohl beim Aufruf der Funktion übertragen als auch beim Beenden der Funktion an die Aufrufstelle zurückübertragen. Der Wert ist beim Aufrufer während der Ausführung der Funktion undefiniert, das bedeutet, dass die Rückübertragung des Wertes schon vor der Rückkehr des Funktionsaufrufs erfolgen kann. Wie bei C++ müssen ref-Parameter immer direkt auf Variablen zeigen, im Gegensatz zu C++ müssen diese definitiv mit einem Wert belegt sein.

  • Die Angabe out hat bei C++ kein Äquivalent: Sie beschreibt einen Parameter, der lediglich beim Beenden der Funktion an die Aufrufstelle übertragen wird. Auch dieser Parameter muss beim Aufruf auf eine Variable zeigen, diese muss aber nicht einen gültigen Wert enthalten, so dass sie möglicherweise erst vom Funktionsaufruf belegt wird.
    C# bietet noch mehr Möglichkeiten, die aber den Rahmen dieses Artikels sprengen würden. Wer sich als C/C++-Programmierer an die Übergabe einer variablen Anzahl von Parametern à la printf gewöhnt hat, wird sich freuen, dass C# auch dies unterstützt und zwar mit dem params-Schlüsselwort. Aber im Gegensatz zu C/C++ ist alles semantisch sauber festgelegt, und es findet sich kein Hintertürchen für unerwartete Schutzverletzungen.

Properties

Da Properties bei C++ leider nicht vorgesehen sind - ein aus meiner Sicht ärgerlicher Designfehler - soll hier mit einem kleinen Beispiel gezeigt werden, wie einfach das bei C# möglich ist:

class MyClass { 
  public int mX { 
    get 
    { 
      return mnX; 
    } // get() 
    set 
    { 
      mnX = value; 
    } // set() 
  } // mX 
  private int mnX; 
} // MyClass 

Bei der set()-Funktion enthält dabei der Standardname value den übergebenen Wert. Man kann jetzt das Property mX der Klasse MyClass ganz einfach verändern und lesen, so wie man es von Fields gewohnt ist:

MyClass cls = new MyClass(); 
cls.mX = 10; 
int i = cls.mX; 

Über das Weglassen der Definition der set()- oder der get()-Funktion kann entschieden werden, ob das Property nur lesend bzw. nur schreibend angesprochen werden darf.

Indexers

Manchmal möchte man ein Objekt direkt als Feld einsetzen. Bei C++ ist dies durch das Überschreiben des []-operators möglich, aber teilweise sehr kompliziert. Die MFC-Programmierer waren an einigen Stellen damit sichtlich überfordert, beispielsweise bei der CString-Klasse. Bei C# ist das viel einfacher, da hier eine Variante der Property-Definition Verwendung findet. Am einfachsten kann man dies anhand eines Beispiels zeigen:

class MyArray { 
  public MyArray(int c) 
  { 
    manX = new int[c]; 
  } // MyArray 
  public int this[int i] { 
    get 
    { 
      return manX[i]; 
    } // get() 
    set 
    { 
      manX[i] = value; 
    } // set() 
  } // this 
  private int[] manX; 
} // MyArray 

Die Syntax sieht etwas ungewohnt aus, man findet sich aber schnell zurecht: Mit this wird eine Art namenslose Indexadressierung definiert, die dann ähnlich wie ein Property-Member (siehe vorheriger Abschnitt) eine get()- und eine set()-Funktion hat. Diese Methoden verwalten dann den Index im Argument von this auf klassentypische Weise, hier ganz einfach durch Adressierung des Fieldwerts manX. Auch hier wird bei der set()-Funktion wieder der Standardname value verwendet.
Die Definition eines solchen Typs mag kompliziert aussehen, die Benutzung ist dafür umso einfacher, wie folgendes Beispiel zeigt:

MyArray arr = new MyArray(5); 
int nX = 3; 
arr[3] = nX; 

Events

Ein Event in C++ abzufangen, das aus einem OLE/COM-Objekt übergeben wird, ist auch für viele erfahrene C++-Programmierer eine echte Herausforderung. Bei C# ist es dagegen ganz einfach, wie das folgende Beispiel zeigt. Zunächst wird eine abstrakte Funktion mit dem Schlüsselwort delegate deklariert, die den Event-Handler beschreibt, dem die Information übergeben wird. Danach mit EventSource die eigentliche Klasse, die das Event auslöst:

public delegate void EventHandler(string str); 
class EventSource { 
  public event EventHandler SendText; 
  public void ThrowEvent() 
  { 
    if (SendText != null) { 
      SendText("Yes!"); 
    } // if 
  } // ThrowEvent()  
} // EventSource 

Durch Aufruf der Methode ThrowEvent() soll dabei das Event ausgelöst werden. Das Abfangen eines Events ist etwas trickreich, da die Architekten von C# die sehr nützliche Idee hatten, dass man einen Event an vielen Stellen gleichzeitig abfangen können soll. Das ganze wird mit einer unsichtbaren Liste verwaltet, der man mit den Operatoren += und -= Elemente hinzufügen oder wegnehmen kann:

EventSource evsrc = new EventSource(); 
evsrc.SendText += new EventHandler(ReceiveEvent); 
evsrc.ThrowEvent(); 
evsrc.SendText -= new EventHandler(ReceiveEvent); 
evsrc.ThrowEvent(); 
public static void ReceiveEvent(string str) 
{ 
  Console.WriteLine(str); 
} // ReceiveEvent() 

Der zweite Aufruf von ThrowEvent hat keine weitere Ausgabe von "Yes" mehr zu Folge, weil der Event-Handler im Aufruf davor mit -= entfernt wurde.

Modifier für Klassen und Member

Modifier legen für Klassen bzw. Strukturen und Member von Klassen oder Strukturen deren Verhalten genauer fest. Bei C# hat dabei jedes Member seinen eigenen Modifier, Gruppenangaben wie protected: in C++ kennt C# nicht.
Für Klassen sind folgende Modifier vorgesehen:
new erlaubt das Ausblenden einer Methode der Basisklasse mit gleichem Namen, ohne dass diese virtuell sein muss. Bei C++ ist eine Angabe von new nicht erforderlich mit der ärgerlichen Konsequenz, dass man als Entwickler der Basisklasse bei der späteren Weiterentwicklung und Hinzufügung von Funktionen nie so richtig weiß;, ob jemand anderes in der Ableitung zufällig die gleiche Funktion verwendet hat. Bei C# gibt es dagegen eine Fehlermeldung für den Entwickler der abgeleiteten Klasse und mit new kann er auf eigene Verantwortung diesen Fehler wieder ausschalten.
abstract verbietet, dass von einer Klasse direkt ein Objekt instantiiert werden kann. Dafür kann die Klasse dann Methoden enthalten, die zwar deklariert aber nicht definiert sind (das ist nebst den Delegates die einzige Stelle, wo diese Unterscheidung in C# wichtig ist). Erst durch Ableitung der Klasse und dortiger Vervollständigung der Definitionen kann die Klasse zum Anlegen von Objekten benutzt werden. Im wesentlichen entspricht dies der Zuweisung von Methoden mit "= 0" bei C++, nur ist durch das separate Schlüsselwort abstract die Bedeutung einer Klasse dem Anwender viel schneller bewusst.
seal ist etwa das Gegenteil von abstract: Man kann zwar die Klasse beliebig zum Instantiieren benutzen, darf sie aber nicht mehr ableiten. Jede Veränderung dieser Klasse in ihrem Verhalten ist damit abgeschlossen, der Entwickler kann sich sicher sein, dass er in der Implementierung herumpfuschen kann, ohne dass deshalb andere Programmteile, welche die Klasse benutzen, beeinträchtigt werden.
Bei Members von Klassen und Strukturen können wesentlich mehr Modifier angegeben werden:
const vor einem Mitglied beschreibt eine Konstante, die direkt in der Klassendefinition gesetzt wird und nirgendwo mehr verändert werden kann. Der Compiler kann diesen Wert direkt benutzen, es ist nicht erforderlich, dass er hierfür Speicher anlegt.
readonly habe ich bei C++ schon häufig vermisst: Hiermit wird bei C# angegeben, dass ein Member als konstant definiert ist, aber im Konstruktor noch geändert werden darf.
static beschreibt wie bei C++, dass das Member für alle Klasseninstantiierungen nur einmal vorhanden ist.
event gibt an, dass es sich bei dem Member um ein Event handelt.
virtual muss angegeben werden, um wie bei C++ eine virtuelle Methode zu beschreiben. Die Methode kann dann von einer Ableitung überschrieben werden, wozu in dieser dann ...
overwrite angegeben werden muss, um kenntlich zu machen, dass eine bereits vorhandene Funktion ein neues Verhalten erhält.
abstracte gibt bei einer Methode an, dass sie nur deklariert aber nicht definiert ist. Dieser Modifier darf nur angegeben werden, wenn auch die Klasse bzw. Struktur selbst dieses Schlüsselwort angegeben hat.
extern wird für die Unterstützung von "Legacy"-Code verwendet, dem Compiler wird hiermit mitgeteilt, dass die Implementierung der Funktion das Win32-API oder COM/OLE-Objekt aufruft. Darauf gehe ich am Ende des Artikels noch näher ein.
Wie man sieht, geht C# mit der Überschreibung von Members bei Ableitungen sorgfältiger um als C++. So muss der Entwickler beim Überschreiben von Funktionen in abgeleiteten Klassen dem Compiler mitteilen, was er eigentlich will. Bei C++ ist das alles nicht wichtig, da hier die Compilierung in einem Guss gemacht wird; bei C# dagegen können im Rahmen von .NET Ableitungen über Komponentengrenzen hinweg gemacht werden, die Entwickler der beiden Komponenten haben sich vielleicht noch nie gesehen und aus diesem Grund ist die Erhöhung der Klarheit bei solchen Angaben sehr wichtig für die Stabilität der Komponentenbenutzung bei Updates und Weiterentwicklungen.

Modifier für die Zugriffssteuerung

Ähnlich wie bei C++ unterstützt auch C# die Einschränkung auf den Zugriff von Members innerhalb von Klassen oder Strukturen. Hiermit wird festgelegt, welche Member bei der Instantiierung verwendet werden können, welche nur innerhalb von Ableitungen und welche nur innerhalb der Klasse selbst.
Bei .NET werden Klassen auch direkt für die Definition von Komponenten verwendet. Man möchte aber sicherlich nicht jede Klasse innerhalb einer Komponente als Exportschnittstelle einer Klasse festgelegt haben.
Aus all diesen Gründen gibt es für Klassen und Members folgende Modifier:
public gibt an, dass die Klasse bzw. das Member überall benutzt werden kann.
protected gibt an, dass die Klasse bzw. das Member nur in der Definition der Klasse selbst bzw. in einer Ableitung der Klasse benutzt werden kann.
private verbietet wie bei C++ eine Benutzung auß;erhalb der Klasse selbst.
internal schließ;lich legt fest, dass die Klasse bzw. das Member nur innerhalb der Komponente aufgerufen werden kann, also nicht ein Bestandteil einer Komponenten-Exportschnittstelle ist.

Anweisungen unter C#

Wie Sie bis jetzt gesehen haben, gibt es bei der Definition von Variablen, Typen, Klassen usw. doch eine ganze Reihe von Abweichungen zwischen C++ und C#. Bei Anweisungen (Statements) ist das anders: Hier wurden die bereits bewährten Anweisungen von C++ weitgehend unverändert übernommen, auch die sind bereits von C weitgehend übernommen worden, so dass es sich mal wieder zeigt, wie weitsichtig die Väter von C waren, als sie sich diese Anweisungen ausdachten.
Es gibt daher die bekannten Anweisungen if, switch, for, while, do und goto auch bei C#. Bei if, while und do ist zu beachten, dass die Bedingungsargumente einen bool-Typ erwarten und es kein automatisches Casting aus Integer gibt. So wird der Aufruf

int i = 1; 
if (i) { // Fehler hier! 
  ... 
} // if 

vom Compiler mit Fehler CS0029 zurückgewiesen; korrekt müsste er lauten:

int i = 1; 
if (i != 0) { 
  ... 
} // if 

Das mag für erfahrene C++-Programmierer etwas umständlich erscheinen, hat aber nicht nur den Vorteil der besseren Lesbarkeit sondern vermeidet ein für allemal den Fehler, dass der Programmierer

int i = 1; 
if (i == 0) { 
  ... 
} // if 

schreiben wollte, aber versehentlich

int i = 1; 
if (i = 0) { 
  ... 
} // if 

eingegeben hat. Solche Fehler sind in C++ schwer zu finden, bei C# einfach nicht mehr möglich und das ist ein groß;er Fortschritt.

Die Verbesserung von switch

Eine der größ;ten Designfehler von C ist das Durchfallen der Ausführung zur nächsten case-Selektion einer switch-Anweisung. Das Programm wird in der nächsten Selektion fortgesetzt, wenn das break fehlt. Es ist eine meiner häufigsten Fehlerursachen, wenn Programme völlig anders als geplant laufen und es ist mir ein Rätsel, warum diese Schwachstelle bei C++ nicht beseitigt wurde. Bei C# ist das anders, da muss jetzt bei jedem case-Eintrag am Ende ein break stehen, sonst gibt der Compiler eine Fehlermeldung aus.
Nun ist es aber so, dass dieses Durchfallen manchmal sehr praktisch ist und man damit die Übersichtlichkeit des Programms auch durchaus erhöhen kann. Die Architekten von C# haben diesen Interessentenskonflikt auf sehr elegante Weise gelöst: Sie erlauben alternativ zum break auch die Angabe von goto unter Angabe einer case-Sprungadresse als Argument. Das kann man am besten anhand eines Beispiels zeigen:

switch (i) { 
  case 1: 
     goto case 2; 
  case 2: 
     goto defau<  
  case 3: 
     break; 
  default: 
     goto case 3; 
} // switch 

Eine weitere häufig bei C und C++ vermisste Erweiterung der switch-Anweisung ist die Verwendung von konstanten Zeichenfolgen. Es ist für den Compiler eine Kleinigkeit, die Sprungtabelle hierfür aufzubauen, so dass ich auch nicht so ganz verstehen kann, warum so etwas nicht in C++ eingebaut wird. So sieht das in C# aus:

string strEvent = "System Journal"; 
switch (strEvent) { 
  case "System Journal": 
     break;  
  case "Dr. Dobbs" : 
     break;  
  default: 
     break; 
} // switch 

Neue C#-Anweisung: foreach

Das Durchschreiten einer Aufzählung von Objekten mit for oder while war noch nie gut lesbar. Die C++-STL oder Visual-Basic bieteten hierfür eine For-Each-Anweisung an und C# hat diese Idee auch aufgegriffen. Die Anweisung ist standardmäß;ig für Felder und Datensammlungen (Collections) verfügbar, kann aber auch auf eigene Klassen angewendet werden, sofern bestimmte Aufzählungsmethoden vorhanden sind. Hier ein Beispiel für die Anwendung auf ein Feld von Integerwerten:

int cOdd = 0; 
int[] anNum = new int [] {17,92,8,7,12,19}; 
foreach (int n in anNum) { 
  if (n % 2 != 0) { 
    cOdd++;  
  } // if 
} // foreach 
// cOdd ist jetzt 3 

Wenn Sie foreach für eine eigene Klasse ermöglichen wollen, dann müssen Sie die Methoden GetEnumerator und MoveNext zur Verfügung stellen - und schon wird sich der Anwender Ihrer Klasse über deren gut lesbare Anwendung freuen.

Behandlung von Ausnahmen

Eine von der Regel abweichende Ausführung eines Programms ist bei interaktiven Programmen, die mit vielen Ressourcen gleichzeitig operieren müssen, leider ein Teil der Normalität. Solche Programme kann man nicht einfach, wie früher bei Konsolenprogrammen üblich, einfach mit einer Fehlermeldung beenden. Man muss alle Fehlerzustände sorgfältig abfangen, den Originalzustand des Programms wieder herstellen und dann die Ausführung fortsetzen. Umso wichtiger ist es, dass der für Ausnahmesituationen zuständige Code leicht aufgerufen und verwaltet werden kann.
Bei C# wurde dabei im Prinzip die Ausnahmebehandlung von C++ mit try, catch und throw übernommen, so dass ich darauf jetzt im Detail nicht mehr eingehen muss. Neu ist allerdings der finally-Block, den man vom Win32-Standard Exception Handling (SEH) schon kennt. Dieser Block umfasst Anweisungen, die sowohl im Normalfall als auch im Ausnahmefall immer ausgeführt werden. In diesem Block stehen üblicherweise Freigaben von Ressourcen, wie etwa das Schließ;en von Dateien. Bei C++ fehlt solch ein Block, was ich als Einschränkung empfinde.
Für die Übertragung von Ausnahmen steht die Klasse Exception zur Verfügung, die mittels Ableitungen spezialisiert werden kann. Das .Net-Laufzeitsystem kennt weder Fehlercodes noch HRESULTs noch Funktionen wie GetLastError, sondern übergibt alle Fehlerzustände mittels Ausnahmen, die von Exception abgeleitet sind und mit throw ausgelöst werden.
Das folgende Beispiel erläutert mittels Konsolenausgaben, wie die Ausnahmebehandlung funktioniert:

int nDivisor = ...; 
try { 
  if (nDivisor > 10) { 
    throw new ArgumentOutOfRangeException("Teiler > 10"); 
  } // if 
  int nResult = 10 / nDivisor; 
  Console.WriteLine("Ergebnis ist {0}", nResult); 
} catch (DivideByZeroException /* exc */) { 
  Console.WriteLine("Teiler darf nicht 0 sein"); 
} catch (Exception exc) { 
  Console.WriteLine("Ausnahme: " + exc.ToString()); 
} finally { 
  Console.WriteLine("Berechnung beendet oder abgebrochen"); 
} // try  

Überläufe erkennen oder nicht

Eine interessante Frage in Zusammenhang mit Ausnahmen ist die Behandlung von Überläufen in arithmetischen Ausdrücken: Soll etwa die Addition von

byte b1 = 200; 
byte b2 = 58; 
byte bSum = (byte)(b1 + b2); 

mit dem Resultat 2 fortgesetzt oder eine Ausnahme ausgelöst werden? In der Anfangszeit der Computer waren solche Überlauferkennungen zeitaufwendig und wurden als überflüssig erachtet, mittlerweile können die meisten Prozessoren Überläufe mit ihrer Hardware erkennen, so dass mehr und mehr neue Programmiersprachen wie etwa Ada die Erkennung vorschreiben. Leider hat die generelle Erkennung von Überläufen manchmal auch Nachteile, etwa wenn man Hexzahlentexte in Werte umwandelt und dabei Überläufe ignorieren möchte.
Die Sprachdesigner von C# haben erkannt, dass es für die Praxis sinnvoll ist, die Überlauferkennung je nach Situation einzustellen. Hierzu gibt es folgende Möglichkeiten:

  • Mit der Compileroption /Checked (siehe Tabelle T1) kann man angeben, ob standardmäß;ig auf Überlauf geprüft wird oder nicht.

  • Mit den Blockanweisungen checked und unchecked um eine Gruppe von Anweisungen herum kann man die Ausführung unabhängig vom Standardverhalten beeinflussen, beispielsweise:

    unchecked { 
      byte b1 = 200; 
      byte b2 = 58; 
      byte bSum = b1 + b2; 
    ... 
    } // unchecked 
    

Komponenten definieren und aufrufen

Jetzt habe ich soviel über Typen, Klassen und Anweisungen gesprochen, dass Sie sicherlich fragen, wo denn die Komponentenprogrammierung bleibt. Die Antwort ist vielleicht das beste an .NET und an C#: Sie programmieren Komponenten einfach mit diesen Sprachkonstrukten. Wenn eine Klasse public ist, wird sie von Ihrer Anwendung oder Ihrem Modul als Komponentenschnittstelle exportiert. Ein anderes C#-Modul kann durch using und Angabe des Klassennamens darauf zugreifen. Das ist alles.
Damit ist Komponentenprogrammierung unter .NET und C# einfacher, als wenn Sie im klassischen C/C++ mehrere Module über .H-Dateien miteinander verbinden. Der Zugriff auf exportierte Klassen kann direkt oder per Vererbung erfolgen, all die schönen Dinge, die sie sonst nur innerhalb eines C++-Moduls durchführen können.
Schließ;lich können Sie den Export einer Klasse über Modulgrenzen hinweg mit internal oder protected einschränken (Erläuterung weiter oben im Text); viel mehr gibt es wirklich nicht zu sagen.
Häufig wollen Sie vielleicht mehrere Klassen, auch über Modulgrenzen hinweg zusammenfassen. Ein Beispiel hierfür ist die sehr umfangreiche Klassensammlung System des .NET-Laufzeitsystems. Hierfür steht die Blockanweisung namespace Name {...} zur Verfügung; sie können mehrere dieser Anweisungen ineinander schachteln und damit eine umfangreiche Namenshierarchie aufbauen. APIs, die auf diese Weise definiert sind, sind dann viel übersichtlicher als die flachen APIs von COM/OLE-Komponenten oder das "fast unendlich groß;e" Win32-API.
Beim Zugriff auf solche Klassensammlungen können Sie sich bei using dann gleich auf den ganzen Block beziehen oder eben nur auf einzelne Unterblöcke oder einzelnen Klassen daraus. Mehrdeutigkeiten von Namen beim Zugriff auf viele Module kann man dadurch geschickt vermeiden.

Die Unterstützung von "Legacy"-Code

.NET ist noch nicht als Produkt verfügbar so dass Sie heute auch keine Software entwickeln können, um sie dann zusammen mit der .NET-Umgebung als Komponente oder Anwendung für Windows 95/98/NT/2000 ausliefern zu können. Doch das wird sich spätestens Ende des Jahres 2001 ändern und dann steht eine neue Frage im Raum: Ist .NET eine "Insel der Seligen" oder kann man zwischen .NET und COM/OLE-Komponenten oder gar Win32-Code Daten austauschen und Funktionen aufrufen?
Die Antwort ist uneingeschränkt Ja. .NET unterstützt die Kommunikation zu "Legacy"-Code wie COM/OLE und Win32 in beide Richtungen und C# bietet umfangreiche Sprachkonstrukte dafür an, damit diese Kommunikation so einfach wie möglich realisiert werden kann. Ich möchte an dieser Stelle nicht im Detail darauf eingehen, Ihnen aber wenigstens die gegebenen Möglichkeiten aufzählen:

  • Wenn Sie eine .NET-Komponente entwickelt haben, können Sie diese mit wenig Aufwand in eine COM/OLE-Komponente umwandeln indem Sie das .NET-Hilfsprogramm RegAsm verwenden. Es analysiert die exportierten .NET-Schnittstellen und erzeugt den erforderlichen Eintrag in der Registry, so dass das .NET-Modul wie ein COM/OLE-Modul erscheint. Auch erzeugt es den erforderlichen Wrapper-Code, damit die Aufrufe aus COM/OLE (Flat COM oder OLE Automation) auf unkomplizierte und korrekte Weise umgesetzt werden, zum Beispiel bei der Konvertierung von Strings und SafeArray-Objekten.

  • Wenn Sie COM/OLE-Komponenten aus .NET-Anwendungen aufrufen wollen, benötigen Sie nur die .TLB-Datei der Komponente. Diese können Sie mit dem .NET-Hilfsprogramm TlbImp in eine .NET-Komponente mit Namen RCW ("Runtime Callable Wrapper") konvertieren, die dann die Übertragung der Parameter zu COM/OLE durchführt. Dies sind sogenannte statische COM/OLE-Aufrufe, weil Sie dies als Entwickler einmalig vorbereiten.

  • Über die Klasse System.Reflection der .NET-Laufzeitumgebung können Sie schließ;lich auch dynamisch auf solche Komponenten zugreifen, also prinzipiell während der Ausführung Ihres Programms Komponenten ansprechen, die Sie während der Entwicklung noch gar nicht kannten.

  • ·Schließ;lich gibt es auch die Möglichkeit, traditionellen Win32-Code aus C#-Programmen aufzurufen, wobei sich der Compiler um Dinge wie die Anpassung von Zeichenfolgen von Unicode in ASCII kümmert. Zur Erläuterung einfach ein kleines Beispiel, wie die bekannte Win32-API-Funktion MessageBox aus einem C#-Programm aufgerufen werden kann:

// Definition der Win32-Funktion in C# 
[sysimport(dll="user32.dll")] 
  public static extern int MessageBoxA( 
    int hwnd, string  strMsg, string strCaption, 
    int nType); 
// Aufruf der Funktion in C# 
int nMsgBoxResult = MessageBoxA(0, "Hello C#", "SJ", 0); 

Eine abschließ;ende Bewertung

Ich habe versucht, Ihnen zahlreiche Vorteile von C# gegenüber C++ aufzuzeigen. Mein Eindruck ist, dass C# eine professionelle, voll praxistaugliche Sprache ist und nicht nur ein Java-Ersatz von Microsoft. Ich bin nicht auf alle Möglichkeiten der Sprache eingegangen, so bietet C# auch das von C++ bekannte Überladen von Operatoren an, man kann sogar unsicheren oder von der Garbage Collection ausgeschlossenen Code in C#-Programmen verwendet. Dies sind weitere Beispiele für die Praxistauglichkeit von C#.
C# zeigt viele Möglichkeiten von .NET auf, die allerdings auch in anderen Sprachen von .NET (Visual Basic und C++ mit managed Code) zu finden sind. Entscheiden Sie selbst, ob Sie als C++-Programmierer auf C# wechseln wollen oder nicht - aus meiner Sicht hängt bei so einer Entscheidung viel davon ab, ob Sie mit einem Projekt weitgehend von vorne anfangen oder eher viel bestehenden C++-Code einbinden müssen, was eine Adaptation eher erschwert und schnell zum C#-Frust führen kann.
Viele der Neuerungen von C# fehlen mir jetzt zunehmend bei C++, zum Beispiel der fehlende finally-Block bei Ausnahmenbehandlungen oder das Problem des Durchfallens bei der switch-Anweisung, wenn man das break vergessen hat. Was ich jetzt gerne hätte wäre eine Übersprache von C# und C++, die 100% aufwärtskompatibel zu C# ist, aber auch die Möglichkeiten von C++ bietet. Vielleicht heiß;t ja das nächste C++ von Microsoft "C*".
Welche C++-Möglichkeiten sind das? Was mir persönlich an C# fehlt, sind Templates und eine Adaption der STL. Ich bin von der STL sehr angetan, habe sehr lange gebraucht, um damit effizient arbeiten zu können und möchte mich wegen C# nicht davon trennen. Das gleiche gilt für Stream-I/O.
Bedenken Sie auch, dass die die Entwicklung der Sprache, wie auch des ganzen .NET-Systems, noch nicht abgeschlossen ist und dass sich noch einiges ändern kann, auch was die Zuverlässigkeit und Stabilität der Werkzeuge angeht. Vielleicht wird C# auch noch hier und da erweitert.
Ich denke aber, dass Microsoft mit .NET und C# gezeigt hat, dass sie auf die heutigen Anforderungen der PC-Software-Programmierung eine Antwort gefunden hat und dass .NET eine vielversprechende Plattform für die Programmierung "globaler Anwendungen" sein wird, die im Internet weltweit verteilt laufen werden und miteinander automatisch kommunizieren. Bei der Entwicklung solcher Anwendungen wird sicherlich C# als Programmiersprache eine bedeutende Rolle spielen.

Marcellus Buchheit ist Leiter der Forschung und Entwicklung und Aufsichtsratsvorsitzender der Firma WIBU-SYSTEMS AG in Karlsruhe und beschäftigt sich seit 24 Jahren mit der Programmierung von Computern. Er kann unter mabu@wibu.com erreicht werden.