29.04.2014 Aufrufe

Leseprobe

Leseprobe

Leseprobe

MEHR ANZEIGEN
WENIGER ANZEIGEN

Erfolgreiche ePaper selbst erstellen

Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.

C# – first guide ISBN 3-8272-5936-3<br />

Danksagung<br />

Was würden Sie erwarten, wenn Ihnen ein Verleger per E-Mail mitteilt, dass er<br />

sich mit Ihnen über ein wirklich spannendes Buchprojekt unterhalten will? Als<br />

Chris Webb mich schließlich anrief, hatte ich alles Mögliche erwartet, nur nicht<br />

dieses Buch. Sein Angebot klang zu verlockend, um es ablehnen zu können: Wer<br />

hat schon die Chance, ein Buch über eine wirklich neue Programmiersprache zu<br />

schreiben?<br />

Mein Dank geht natürlich an Chris Webb und Brad Jones, dafür, dass sie bei dem<br />

knapp bemessenen Zeitrahmen ihr Vertrauen in mich gesetzt haben. Beide, aber<br />

auch mein Lektor, Kevin Howard, mussten sich mit meinen konzeptuellen Änderungen<br />

im Inhalt abfinden, als das Buch zunehmend Gestalt annahm – man kann<br />

alte Gewohnheiten eben nicht ablegen.<br />

Speziell danken möchte ich Christian Koller, dafür dass er meine Kapitel inhaltlich<br />

gegengelesen und mich darauf hingewiesen hat, wenn wieder einmal Details<br />

fehlten, die sich ohne weitere Erläuterung nur C++-ProgrammiererInnen erschließen.<br />

Obwohl ich bei Sams Publishing nicht jeden persönlich kenne, der an diesem<br />

Buch beteiligt war, danke ich trotzdem allen, die ihren Teil dazu beitragen haben,<br />

dass es letztlich seinen Weg in die Regale gefunden hat – trotz des engen Zeitrahmens.<br />

Darüber hinaus gilt mein letzter und wichtigster Dank meiner Familie für ihre<br />

unablässige Unterstützung während all meiner Buchprojekte.<br />

Über den Autor<br />

Christoph Wille, MCSE, MCSD, CNA und MCP-IT, arbeitet als Netzwerkberater<br />

und Programmierer speziell für Windows DNA. Von Microsoft als »Most Valuable<br />

Professional« (MVP) für den Bereich Active Server Pages eingestuft, zählt<br />

er zu den wenigen Entwicklern, die mit Microsoft zusammen an den ersten Versionen<br />

der Sprache C# gearbeitet haben.<br />

Christoph Wille ist Autor oder Coautor verschiedener Bücher, darunter: Sams<br />

Teach Yourself ADO 2.5 in 21 Days, Sams Teach Yourself Active Server Pages in<br />

24 Hours, MCSE Training Guide: SQL Server 7 Administration und Sams Teach<br />

Yourself MCSE TCP/IP in 14 Days.


Einführung


14<br />

Dieses Buch ist Ihre Fahrkarte für den schnellen Einstieg in die neue komponentenorientierte<br />

Programmiersprache C# (gesprochen: C-Sharp), die Microsoft dem<br />

Produkt Next Generation Windows Services Runtime beilegt.<br />

Bei dem Produkt, das hier als NGWS-Subsystem oder -Laufzeitumgebung<br />

bezeichnet wird, handelt es sich um eine Laufzeitumgebung, die nicht nur die<br />

Ausführung von Code verwaltet, sondern auch eine Reihe von Diensten bereitstellt,<br />

um die Programmierung zu vereinfachen. Ein Compiler, dessen Code in der<br />

NGWS-Laufzeitumgebung ausführbar sein soll, muss sogenannten verwalteten<br />

Code produzieren. Gewissermaßen im Gegenzug gibt es dafür die sprachübergreifende<br />

Ausnahmebehandlung sowie Komponentenintegration, verbesserte Sicherheit,<br />

Versionskontrolle und Profiling- sowie Debugging-Dienste.<br />

C# ist die erste Sprache, die speziell für die NGWS-Laufzeitumgebung entwickelt<br />

wurde. Ja, ein guter Teil des gesamten NGWS-Subsystems ist bereits in C#<br />

geschrieben. Der C#-Compiler dürfte daher auch der am besten getestete und<br />

optimierte Compiler sein, der dem NGWS beigepackt ist. C# bezieht zwar einen<br />

großen Teil seiner Konzepte von C++, ist aber moderner und bietet wesentlich<br />

mehr Typensicherheit. Mit anderen Worten, die Sprache hat die besten Voraussetzungen,<br />

das Mittel der Wahl für die Implementation von Unternehmenslösungen<br />

zu werden.<br />

Wer sollte dieses Buch lesen?<br />

Wenn Sie gerade dabei sind, die Programmierung zu erlernen, ist dieses Buch<br />

wahrscheinlich eher nichts für Sie. Es wendet sich vielmehr an Programmierer-<br />

Innen, die einen schnellen Überblick über die Möglichkeiten der Sprache erhalten<br />

wollen und dafür bereits das nötige Vorverständnis von anderen Programmiersprachen<br />

(vorzugsweise von C, C++ oder Java, aber auch Visual Basic) her mitbringen.<br />

Am leichtesten wird sich Ihnen C# erschließen, wenn Sie C++ als Hintergrund<br />

haben; aber auch wenn Sie in einer anderen Sprache flüssig programmieren können,<br />

wird Ihnen das Buch die wichtigsten Aspekte und Konzepte von C# vermitteln.<br />

Am meisten werden Sie von dem Buch profitieren, wenn Sie auch ein wenig<br />

Ahnung von der COM-Programmierung haben – obwohl die COM-Programmierung<br />

beileibe nicht als Voraussetzung für ein generelles Verständnis der Sprache<br />

und des Buchs zu sehen ist.


• 15<br />

Aufbau des Buchs<br />

Das Buch umfasst zwölf Kapitel. Im Folgenden eine kurze Übersicht, was Sie in<br />

den einzelnen Kapiteln erwartet:<br />

• Kapitel 1, »Einführung in C#« – nimmt Sie auf einen kurzen Rundgang durch<br />

die Sprache mit und beantwortet die Fragen, wann und warum es sich lohnt, C#<br />

zu erlernen.<br />

• Kapitel 2, »Der Unterbau – das NGWS-Laufzeitsystem« – stellt Ihnen das<br />

Drumherum sowie die NGWS-Laufzeitumgebung als Infrastruktur für die<br />

Ausführung von C#-Code vor.<br />

• Kapitel 3, »Die erste C#-Anwendung« – in diesem Kapitel lernen Sie Ihre erste<br />

C#-Anwendung kennen, sie trägt (wie sollte es anders sein) den Namen »Hello<br />

World«.<br />

• Kapitel 4, »Die Typen von C#« – das Typensystem von C# ist reichhaltig; in<br />

diesem Kapitel lernen Sie neben den elementaren Datentypen der Sprache den<br />

wichtigen Unterschied zwischen wertbehafteten Typen und Referenztypen sowie<br />

den Mechanismus des Boxing und Unboxing kennen.<br />

• Kapitel 5, »Klassen« – die in diesem Kapitel vorgestellte objektorientierte Programmierung<br />

mit Klassen vermittelt Ihnen einen Hauch der gewaltigen Ausdruckskraft<br />

der Sprache. Hier kommen Sie mit Konstruktoren, Destruktoren,<br />

Methoden, Eigenschaften, Indizierern und Ereignissen in Kontakt.<br />

• Kapitel 6, »Kontrollstrukturen« – die Logik eines Programms spiegelt sich im<br />

Fluss der Anweisungen wieder. In diesem Kapitel lernen Sie die Mittel kennen,<br />

um in C# Fallunterscheidungen und Schleifen zu formulieren.<br />

• Kapitel 7, »Ausnahmen behandeln« – in diesem Kapitel erfahren Sie, wie Sie<br />

in Ihrem Code Ausnahmen behandeln und selbst signalisieren, damit sich Ihr<br />

Programm in der NGWS-Welt als »gesellschaftstauglich« erweist.<br />

• Kapitel 8, »Komponenten mit C# schreiben« – zeigt Ihnen, wie Sie Ihre eigenen<br />

Komponenten in C# für die sprachübergreifende Nutzung im Rahmenwerk<br />

des NGWS implementieren.<br />

• Kapitel 9, »Konfiguration und Verbreitung« – hier erfahren Sie, was es in C#<br />

mit der bedingten Kompilierung auf sich hat und wie Sie Ihren Code auf der<br />

Basis von Kommentaren geeignet dokumentieren, damit der Compiler daraus<br />

eine Dokumentation im XML-Format erzeugen kann. Darüber hinaus stellt Ihnen<br />

dieses Kapitel das Konzept der Versionskontrolle der NGWS-Laufzeitumgebung<br />

vor.


16<br />

• Kapitel 10, »Integration von nicht verwaltetem Code« – auf die NGWS-Laufzeitumgebung<br />

angewiesen zu sein, heißt nicht, auf den Einsatz von existierendem<br />

(nicht verwaltetem) Code verzichten zu müssen. Dieses Kapitel zeigt, wie<br />

das Zusammenspiel funktioniert.<br />

• Kapitel 11, »C#-Code debuggen« – stellt Ihnen den Einsatz und die Möglichkeiten<br />

des SDK-Debuggers für die Fehlersuche in C#-Anwendungen vor.<br />

• Kapitel 12, »Sicherheit« – das NGWS-Laufzeitsystem verfügt über ein zeitgemäßes<br />

Sicherheitskonzept für die Ausführung von Code, der unterschiedlichen<br />

Sicherheitsanforderungen genügt. Hier erfahren Sie, wie »Codezugriffsrechte«<br />

funktionieren und was »rollenbasierte Sicherheit« ist.<br />

Was brauchen Sie für die Arbeit mit diesem Buch?<br />

Um die Beispiele aus diesem Buch auf praktischer Ebene nachzuvollziehen,<br />

benötigen Sie das Next Generation Windows Services (NGWS) Software Development<br />

Kit (SDK) und eine Maschine, auf der (mindestens) Windows 2000<br />

installiert ist. Obwohl Sie vom Prinzip her bereits mit der Kombination NGWS-<br />

Laufzeitumgebung und C#-Compiler auskommen, empfiehlt es sich doch zum<br />

Ausloten der Möglichkeiten dieser neuen Technologie, das gesamte SDK mit<br />

allen Werkzeugen (insbesondere den Debugger) sowie der Sprachdokumentation<br />

zu installieren.<br />

Das Buch verlangt nicht, dass auf Ihrer Maschine irgendwelche Werkzeuge aus<br />

Visual Studio 7 installiert sein müssen. Die einzige Empfehlung in dieser Richtung<br />

ist der Quelltext-Editor; andere Editoren mit einer gewissen Basisfunktionalität<br />

sind aber genauso gut geeignet.


Kapitel 1<br />

Einführung in C#<br />

1.1 Warum eine neue Programmiersprache? 18<br />

1.2 Zusammenfassung 24


18<br />

Warum eine neue Programmiersprache?<br />

Herzlich willkommen in der Welt von C#! Dieses Kapitel nimmt Sie auf einen<br />

kurzen Rundgang durch die Sprache mit und beantwortet Fragen wie »Warum<br />

lohnt es sich, C# zu erlernen?«, »Was sind die wichtigsten Unterschiede zwischen<br />

C++ und C#?« und »In welchem Ausmaß wird C# die Entwicklung Ihrer Software<br />

vereinfachen und Ihnen mehr Spaß bereiten?«.<br />

1.1 Warum eine neue Programmiersprache?<br />

Sicher werden Sie sich diese Frage schon gestellt haben: »Warum soll ich eine<br />

andere Programmiersprache erlernen, wenn ich für meine Unternehmenslösungen<br />

mit C++ oder Visual Basic als Entwicklungssprache bestens zu Rande komme?«<br />

Jemand vom Marketing wird Ihnen darauf die prägnante Antwort geben: »Unsere<br />

Zielvorgabe ist es, dass C# die lingua franca für die NGWS-Anwendungsprogrammierung<br />

im Bereich der Unternehmenslösungen wird«. Der klare Unterton<br />

dieser Aussage ist, dass man bereits in naher Zukunft an dieser Sprache wohl eher<br />

schlecht vorbeikommen wird. Dieses Kapitel wird diese Aussage durch einige<br />

Sachargumente stützen sowie einen Überblick über die interessantesten Sprach-<br />

Features geben – gewissermaßen als Appetitmacher.<br />

Die Programmiersprache C# stammt von C und C++ ab, versteht sich aber als<br />

moderne, einfache, typensichere und vor allem vollständig objektorientierte Programmiersprache.<br />

Falls Ihnen C/C++ vertraut ist, wird es ein Kinderspiel für Sie<br />

sein, C# zu erlernen. So sind viele C#-Anweisungen direkt C++ entlehnt, darunter<br />

das gesamte Ausdrucks- und Operatorwesen. Ja, wer nicht allzu genau hinsieht,<br />

wird ein C#- leicht für ein C++-Programm halten.<br />

Das Attribut »moderne Programmiersprache« ist für C# keine Floskel. C# vereinfacht<br />

C++ in vielerlei Hinsicht, so beim Klassenbegriff, in den Namensbereichen<br />

und beim Überladen von Methoden. Was die Komplexität betrifft, so wurde C#<br />

gegenüber C++ um einiges abgespeckt, mit dem Ergebnis, dass die Sprache leichter<br />

und weniger fehleranfällig geworden ist. So gibt es keine Makros und keine<br />

Mehrfachvererbung mehr; diese Features haben im Allgemeinen mehr Ärger<br />

bereitet als genützt – speziell Entwicklern von Unternehmenslösungen.<br />

Auf der anderen Seite sind aber auch ein ganze Reihe von Features hinzugekommen,<br />

die einem Programmierer das Leben leichter machen sollen: strikte Typensicherheit,<br />

Versionskontrolle, Garbage Collection und vieles mehr. All diese Konzepte<br />

zielen auf die Entwicklung komponentenorientierter Software. Trotzdem –<br />

oder gerade weil – man in C# nicht mehr das volle Ausdruckspotenzial von C++<br />

zur Verfügung hat, verbessert sich die Produktivität.<br />

Um nicht zu viele Merkmale der Sprache auf einmal vorzustellen, werden die folgenden<br />

Abschnitte einzeln auf die genannten Attribute eingehen und die Spracheigenschaften,<br />

die sie rechtfertigen, kurz vorstellen:


Kapitel 1 • Einführung in C# 19<br />

1.1.1 Einfach<br />

• einfach<br />

• modern<br />

• objektorientiert<br />

• typensicher<br />

• mit Versionskontrolle<br />

• kompatibel<br />

• flexibel<br />

Was einem im Zusammenhang mit C++ sicher nicht in den Sinn kommen wird, ist<br />

das Attribut »einfach«. Mit C# ist das anders: »Einfachheit« war in der Tat mit<br />

das wichtigste Ziel bei der Entwicklung dieser Programmiersprache. Ihrethalben<br />

sind eine ganze Reihe von Sprach-Features aus der C/C++-Welt auf der Strecke<br />

geblieben, aber es sind auch welche hinzugekommen.<br />

Ein wirkliches prominentes Konzept, das in C# fehlt, sind Zeiger. Nachdem man<br />

es bei C# standardmäßig mit verwaltetem Code zu tun hat, sind potenziell unsichere<br />

Operationen wie direkte Speicherzugriffe oder Adressmanipulationen nicht<br />

nur »nicht erlaubt«, es fehlen schlicht die sprachlichen Mittel, sie zu formulieren.<br />

Segen oder Fluch? Ersteres natürlich, denn welcher C++-Programmierer kann<br />

schon von sich behaupten, noch nie auf Speicheradressen zugegriffen zu haben,<br />

für die der benutzte Zeiger nicht gedacht war.<br />

In engem Zusammenhang mit dem für Entgleisungen anfälligen Zeigerkonzept<br />

stehen die Operatoren ::, . und -> für die Auflösung von Namensbereichen, Elementen<br />

und Referenzen. Diese Operatoren sind ein weiteres komplexes Kapitel<br />

von C++, für deren Verständnis gut und gerne ein zusätzlicher Tag harter Auseinandersetzung<br />

mit der Sprache zu veranschlagen ist. C# räumt mit dieser verwirrenden<br />

Vielfalt auf und packt die gesamte Funktionalität in einen einzigen Operator,<br />

den Punktoperator. Der Programmierer muss jetzt nur noch das Konzept der<br />

verschachtelten Bezeichner verstehen.<br />

Auch muss man sich bei C# nicht mehr länger mit irgendwelchen kryptischen<br />

Typen herumschlagen, die auf verschiedene Prozessorarchitekturen gemünzt<br />

sind. C# benutzt ein vereinheitlichtes Typensystem, das es gestattet, wirklich<br />

jeden Wert als Objekt zu betrachten, gleich, ob er einen elementaren Typ trägt<br />

oder einer ausgewachsenen Klasse angehört. Im Gegensatz zu anderen Programmiersprachen<br />

ist die Verpackung elementarer Typen als Objekte bei C# nicht mit<br />

erheblichen Laufzeitverlusten behaftet, da die Sprache speziell für diesen Zweck<br />

den sogenannten Boxing-Mechanismus vorstellt. Auf Boxing und Unboxing geht


20<br />

Warum eine neue Programmiersprache?<br />

das Buch an späterer Stelle natürlich noch ausführlicher ein – jetzt nur soviel: Der<br />

Mechanismus erlaubt es, einfache Typen nach Belieben als Objekte zu verpacken<br />

und bei Bedarf wieder auszupacken.<br />

Erfahreneren Programmierern wird es zwar nicht gefallen, die Vorteile sind aber<br />

nicht von der Hand zu weisen: C# behandelt Ganzzahlen (Integerwerte) und<br />

Wahrheitswerte (Boolesche Werte) als von Grund auf unterschiedliche Datentypen<br />

(zwischen denen es insbesondere keine implizite Typumwandlung gibt).<br />

Unbeabsichtigte Zuweisungen in if-Bedingungen oder in Schleifenkriterien, wie<br />

sie in C/C++ häufig Ursache verzwickter Fehler sind, sind in C# nicht möglich,<br />

weil der Compiler einen Fehler meldet, sobald Sie ein Kriterium formulieren, das<br />

keinen Booleschen Wert liefert.<br />

Weiterhin räumt C# mit Redundanzen auf, die sich im Lauf der Jahre in C++ zur<br />

Aufrechterhaltung der C-Kompatibilität eingeschlichen haben – so beispielsweise<br />

mit const vs. #define und der Zweigleisigkeit bei Zeichen. In C# findet man ausschließlich<br />

die jeweils am weitesten verbreitete Alternative dieser Konzepte.<br />

1.1.2 Modern<br />

Der für das Erlernen von C# verbundene Aufwand ist eine wirklich zukunftsträchtige<br />

Investition, da C# eigens als die Programmiersprache für NGWS-<br />

Anwendungen entwickelt wurde. Sie werden daher vieles bereits als »zur Spache<br />

gehörig« vorfinden, was Sie in C++ noch mühsam haben implementieren müssen<br />

oder dort auch gar nicht verfügbar war.<br />

Eine besonders für den Bereich der Unternehmenslösungen willkommene Erweiterung<br />

des Typensystems sind die finanzmathematischen Typen. Sie haben nun<br />

den Datentyp decimal zur Verfügung, der speziell für das Rechnen mit Geldbeträgen<br />

ausgelegt ist, weil er die im Zusammenhang mit Gleitkommawerten auftretenden<br />

Rundungsfehler vermeidet. Darüber hinaus können Sie natürlich jederzeit<br />

Ihre eigenen Datentypen implementieren, falls Sie für Ihre Problemstellung mit<br />

den elementaren Datentypen der Sprache nicht auskommen.<br />

Nachdem Sie – sicherlich ein wenig skeptisch – erfahren mussten, dass C# keine<br />

Zeiger kennt, wird es Sie wahrscheinlich nicht mehr überraschen, dass Sie die<br />

Sprache insbesondere auch von der gesamten Speicherverwaltung entlastet. Das<br />

Speichermanagement übernimmt komplett der Garbage Collector des NGWS-<br />

Laufzeitsystems. Und weil sowohl der Code als auch der Speicher eines C#-Programms<br />

»verwaltet« ist, muss die Sprache schon aus Gründen der Stabilität absolute<br />

Typensicherheit garantieren.<br />

Obwohl die Ausnahmebehandlung für einen C++-Programmierer sicher nichts<br />

Neues ist, wird ihm die zentrale Bedeutung dieses Konzepts unter C# auf den ersten<br />

Blick doch etwas ungewohnt erscheinen. Im Unterschied zu C++ ist der Ausnahmebehandlungsmechanismus<br />

von C# sprachübergreifend konzipiert und wird


Kapitel 1 • Einführung in C# 21<br />

gleichermaßen vom NGWS-Laufzeitsystem gestellt. Die spitzfindigen HRESULT-<br />

Werte von C++ haben ausgedient, die Fehlerbehandlung auf der Basis von Ausnahmen<br />

wird in C# zu einem robusten Mittel für den Programmieralltag.<br />

Im Zeitalter der globalen Vernetzung ist Sicherheit zu einer zentralen Anforderung<br />

für Programmiersprachen geworden – insbesondere für solche, die für sich<br />

das Attribut »modern« in Anspruch nehmen wollen. C# lässt Sie hier nicht im<br />

Regen stehen: Die Sprache bietet von der Syntax her in Form der sogenannten<br />

Metadaten Unterstützung für das zentrale Sicherheitsmodell der NGWS-Laufzeitumgebung.<br />

Eine detailliertere Diskussion dieses Konzepts finden Sie gleich im<br />

nächsten Kapitel.<br />

1.1.3 Objektorientiert<br />

Ein Verzicht auf Objektorientierung dürfte so ziemlich das Letzte sein, was man<br />

heute von einer neuen Programmiersprache erwartet, oder? Selbstverständlich<br />

unterstützt C# sämtliche Schlüsselkonzepte der objektorientierten Programmierung,<br />

darunter Features wie Verkapselung, Vererbung und Polymorphie. Das<br />

gesamte in C# vorgegebene Klassenmodell gründet auf das von der NGWS-Laufzeitumgebung<br />

bereitgestellte virtuelle Objektsystem (VOS), das im nächsten<br />

Kapitel genauer beschrieben wird. Mit anderen Worten, das Objektmodell ist nun<br />

Bestandteil der Infrastruktur und nicht mehr nur der Programmiersprache.<br />

Wie Sie gleich zu Anfang bemerken werden, wenn Sie sich mit der Sprache als<br />

solcher auseinandersetzen, gibt es keine globalen Funktionen, Variablen und Konstanten<br />

mehr. Alles muss in einer Klasse enthalten sein – entweder als statisches<br />

Element (zu einem Typ gehörig) oder als dynamisches Element (zur Instanz eines<br />

Typs gehörig). Das verbessert nicht nur die Lesbarkeit Ihres Codes, sondern beugt<br />

auch potenziellen Namenskonflikten vor.<br />

Die Methoden einer Klasse sind standardmäßig nicht virtuell, das heißt, sie lassen<br />

sich in einer abgeleiteten Klasse nicht überschreiben. Der Hintergedanke dabei<br />

ist, das unbeabsichtigte Überschreiben von Methoden zu verhindern. Damit eine<br />

Methode überschrieben werden kann, muss sie explizit mit dem Modifizierer virtual<br />

deklariert sein. Das hält nicht nur den Umfang der virtuellen Methodentabelle<br />

(vtable) klein, sondern garantiert auch, dass sich Ihr Code versionsgerecht<br />

verhalten kann.<br />

Wer von C++ kommt, ist es gewohnt, die Sichtbarkeit von Elementen über<br />

Zugriffsmodifizierer zu gestalten. Diese Modifizierer, private, protected und<br />

public, finden Sie natürlich auch in C#. Darüber hinaus kennt C# aber noch einen<br />

vierten Modifizierer: internal. Details dazu diskutiert Kapitel 5, »Klassen«.<br />

Haben Sie in C++ jemals bewusst eine Klasse von mehreren Basisklassen abgeleitet?<br />

(ATL-Programmierer bitte weghören, Ihre Stimme zählt hier nicht!) In fast<br />

allen Fällen kommt man mit einer einzige Basisklasse aus. Und wenn man doch


22<br />

Warum eine neue Programmiersprache?<br />

mehrere Basisklassen verwendet, sind die Scherereien im Allgemeinen größer als<br />

der Nutzen. Nicht zuletzt aus diesem Grund ist C# auf eine einzige Basisklasse<br />

beschränkt. Und wenn Sie glauben, auf Mehrfachvererbung nicht verzichten zu<br />

können, bleibt Ihnen immer noch die Möglichkeit, Ihr Polymorphiekonzept über<br />

Schnittstellen zu implementieren.<br />

Nachdem es in C# keine Zeiger gibt, drängt sich die Frage auf, wie C# mit Funktionszeigern<br />

verfährt. Die Anwort lautet: vermittels Delegationen, die gleichzeitig<br />

den Unterbau für das gesamte Ereignismodell der NGWS-Laufzeitumgebung bilden.<br />

Mehr darüber gleichfalls in Kapitel 5, »Klassen«.<br />

1.1.4 Typensicher<br />

Noch einmal zurück zu den Zeigern: Wenn Sie in C++ einen Zeiger haben, steht<br />

es Ihnen frei, für diesen eine Typumwandlung in jeden anderen Datentyp zu<br />

erzwingen. Das heißt, es steht Ihnen vom Prinzip her auch frei, wirklich idiotische<br />

Dinge zu tun, wie einen Wert vom Typ int* (int-Zeiger) in einen Wert vom Typ<br />

double* (double-Zeiger) zu verwandeln. Solange hinter einer solchen Operation<br />

noch ein realer Speicherwert steckt, mag das zwar funktionieren, sinnvoll ist es<br />

aber sicher nicht. Von einer eigens für Unternehmenslösungen entwickelten Sprache<br />

sollte man schon etwas mehr Sicherheit erwarten können.<br />

Nicht zuletzt aus diesem Grund, aber auch um den Garbage Collector zu schützen,<br />

steht die Typensicherheit im Anforderungskatalog von C# ganz weit oben.<br />

Das bedeutet natürlich für Sie, dass Sie sich bei C# an ein paar Regeln halten<br />

müssen, die den Umgang mit Variablen betreffen:<br />

• Variablen müssen grundsätzlich initialisiert werden. Die Elementvariablen von<br />

Objekten setzt zwar der Compiler implizit auf 0, um lokale Variablen von Methoden<br />

müssen Sie sich aber in jedem Fall selbst kümmern. Da der C#-Compiler<br />

nichtinitialisierte Variablen bemängelt, werden schwer aufzufindende<br />

Fehler, wie sie in C/C++ häufig auftreten, bereits im Keim erstickt.<br />

• C# räumt mit unsicheren Typumwandlungen ein für alle Mal auf. Der C#-<br />

Compiler schließt die Typumwandlung von einem wertbehafteten Typ in einen<br />

Referenztyp aus (das Boxing ausgenommen) und überprüft bei der Typumwandlung<br />

von Objektreferenzen rigide, ob das jeweilige Objekt wirklich von<br />

der Klasse abstammt, die für die als Linkswert fungierende Objektvariable vereinbart<br />

wurde.<br />

• C# stellt die Einhaltung von Array-Grenzen sicher. Im Gegensatz zu C/C++ ist<br />

es nicht mehr möglich, »Elemente« jenseits der Arraygrenzen anzusprechen<br />

und in Speicherbereiche zu schreiben oder daraus zu lesen, für die keine Belegung<br />

erfolgt ist.<br />

• Arithmetische Operationen können Überläufe produzieren. C# bietet eine<br />

Überlaufkontrolle auf Anwendungsebene oder auf der Ebene einzelner Anwei-


Kapitel 1 • Einführung in C# 23<br />

sungen. Bei eingeschalteter Überlaufkontrolle werden Überlauffehler als Ausnahmen<br />

signalisiert, ansonsten jedoch ignoriert.<br />

• C# stellt sicher, dass Referenzparameter (call-by-reference) typensicher sind.<br />

1.1.5 Mit Versionskontrolle<br />

Das Problem, dass bei älteren Programmen nach dem Versions-Update einer<br />

gemeinsam genutzten DLL oft gar nichts mehr läuft, ist nicht neu. Sicher werden<br />

auch Sie ein Lied davon singen können. Es kommt daher, dass jedes Programm<br />

eine neue Version einer DLL installieren kann, mit der dann alle anderen Programme<br />

leben müssen, ob sie können oder nicht. Versionsabhängigkeit ist ein<br />

echtes Problem, das nach einer systemseitigen Lösung verlangt.<br />

Wie in Kapitel 9, »Konfiguration und Verbreitung« näher ausgeführt, ist die<br />

NGWS-Laufzeitumgebung auf eine Versionskontrolle für gemeinsam genutzte<br />

Komponenten eingerichtet, und C# wartet natürlich mit der nötigen Unterstützung<br />

für diesen Mechanismus auf. Obwohl C# selbst keine Garantie bieten kann, dass<br />

eine Anwendung die richtigen Versionen gemeinsam genutzter Komponenten zu<br />

sehen bekommt, so schafft es doch zumindest die Voraussetzungen, dass eine Versionskontrolle<br />

überhaupt stattfinden kann. Die Versionskontrolle bietet einem<br />

Entwickler eine weitgehende Sicherheit, dass die Pflege seines Codes die binäre<br />

Kompatibilität mit bestehenden älteren Anwendungen nicht gefährdet.<br />

1.1.6 Kompatibel<br />

C# ist keine geschlossene Welt. Die Sprache erlaubt den Zugriff auf verschiedene<br />

APIs, allem voran auf solche, die der NGWS Common Language Specification<br />

(CLS) genügen. Die CLS definiert einen Standard für die sprachübergreifende<br />

Verwendung von Code. Um die CLS-Verträglichkeit eines Datentyps (Klasse)<br />

durchzusetzen, überprüft der C#-Compiler alle als öffentlich exportierten Größen<br />

und erzeugt eine Fehlermeldung, wenn die CLS-Verträglichkeit nicht gegeben ist.<br />

Natürlich werden Sie auch mit älteren COM-Objekten arbeiten wollen. Das ist<br />

ohne weiteres möglich, ja, der Zugriff auf COM-Objekte im Rahmen der NGWS-<br />

Laufzeitumgebung ist sogar vollständig transparent. Auf die Integration älteren<br />

Codes geht Kapitel 10, »Integration von nicht verwaltetem Code«, näher ein.<br />

Bei der OLE-Automatisierung liegt der Fall anders. Wer jemals ein OLE-Automatisierungs-Projekt<br />

in C++ realisiert hat, wird von den Automatisierungsdatentypen<br />

seine Finger nicht mehr lassen wollen. Erfreulicherweise bietet C# dafür<br />

eine brauchbare Unterstützung – und zwar ohne Sie großartig mit den Details zu<br />

belästigen.<br />

Schließlich bietet Ihnen C# auch noch die Möglichkeit, im C-Stil vorliegende<br />

API-Funktionen anzusprechen. In der Tat können Sie von C# aus jeden Eintritts-


24<br />

Zusammenfassung<br />

punkt in eine DLL nutzen, solange er der C-Aufrufkonvention folgt. Der Mechanismus,<br />

der den Zugriff auf solchen Code ermöglicht, trägt den Namen Platform<br />

Invocation Services (PInvoke) und wird in Kapitel 10 anhand einiger Beispiele,<br />

die den Aufruf von C-API-Funktionen demonstrieren, näher vorgestellt.<br />

1.1.7 Flexibel<br />

Der letzte Absatz des vorigen Abschnitts wird bei einem C-Programmierer sicher<br />

die Frage aufgeworfen haben, was eigentlich mit API-Funktionen ist, deren Aufruf<br />

die Angabe von Zeigern erforderlich macht. Das ist in der Tat ein Problem.<br />

Und es beschränkt sich leider nicht nur auf einige wenige API-Funktionen, sondern<br />

gilt dummerweise für den Löwenanteil. Mit anderen Worten: Der Zugriff auf<br />

Win32-Code ist ohne das unsichere klassische Zeigerkonzept schlicht nicht zu<br />

bewerkstelligen (obwohl einige API-Funktionen auch über die COM- und PInvoke-Unterstützung<br />

zugänglich sind).<br />

Die Lösung ist nicht gerade schön, aber sie funktioniert: Obwohl C#-Code standardmäßig<br />

im sogenannten sicheren Modus übersetzt wird, kennt der C#-Compiler<br />

auch einen unsicheren Modus, in dem die Deklaration von Zeigern, Strukturen<br />

und statischen Arrays im klassischen Stil von C erlaubt ist. Sowohl der sichere<br />

Code als auch der unsichere Code kommen unterschiedslos im verwalteten<br />

Bereich zur Ausführung, ohne dass zwischen beiden ein Marshaling stattfinden<br />

würde.<br />

Was aber sind die Implikationen für den Umgang mit Speicher, den Sie im unsicheren<br />

Modus anfordern? Nun, der Garbage Collector wird natürlich die Finger<br />

von diesem Speicher lassen. »Unsichere« Variablen aber steckt der Garbage<br />

Collector in denselben Speicherpool wie die »sicheren«.<br />

1.2 Zusammenfassung<br />

Die Sprache C# stammt von C und C++ ab und ist speziell für Entwickler von<br />

Unternehmenslösungen gedacht, die gewillt sind, etwas von der gewaltigen Ausdruckskraft<br />

der Sprache C++ gegen erheblich mehr Sicherheit, Bequemlichkeit<br />

und verbesserte Produktivität einzutauschen. C# ist eine moderne, objektorientierte<br />

und typensichere Sprache, die sich zwar eine Menge von C und C++ ausborgt,<br />

jedoch für spezifische Konzepte, wie Namensräume, Klassen, Methoden<br />

und Ausnahmebehandlung, durchaus eigene Wege geht.<br />

C# kann mit einer Reihe an bequemen Features aufwarten, darunter Garbage Collection,<br />

Typensicherheit, Versionskontrolle und vieles mehr. Gewissermaßen im<br />

Gegenzug bleibt dabei das Zeigerkonzept auf der Strecke, sofern Sie Ihren Code<br />

entsprechend der Standardvorgabe für den sicheren Modus übersetzen. Insbesondere<br />

für die Typensicherheit macht sich das natürlich bezahlt. Falls Sie aber dennoch<br />

Zeiger benötigen, haben Sie keine andere Wahl, als auf den unsicheren


Kapitel 1 • Einführung in C# 25<br />

Modus auszuweichen. Beachten Sie jedoch, dass dann kein Marshaling zwischen<br />

sicherem und unsicherem Code stattfindet.


Kapitel 2<br />

Der Unterbau – das NGWS-<br />

Laufzeitsystem<br />

2.1 Das NGWS-Laufzeitsystem 28<br />

2.2 Das virtuelle Objektsystem (VOS) 33<br />

2.3 Zusammenfassung 40


28<br />

Das NGWS-Laufzeitsystem<br />

Nachdem Sie nun bereits einen ersten Eindruck von C# bekommen haben, stellt<br />

Ihnen dieses Kapitel das NGWS-Laufzeitsystem vor. C# ist für die Ausführung<br />

von Programmen auf dieses Laufzeitsystem angewiesen, weshalb es sich empfiehlt,<br />

die Arbeitsweise und Konzeption dieses Systems besser kennen zu lernen.<br />

Dieses Kapitel organisiert sich in zwei große Abschnitte – einen Grundlagenteil<br />

sowie einen Teil, der die Implikationen für den alltäglichen Umgang vorstellt. Die<br />

Teile überschneiden sich inhaltlich ein wenig, was ein Verständnis der Konzepte<br />

sicher erleichtert.<br />

2.1 Das NGWS-Laufzeitsystem<br />

Das Subsystem der Next Generation Windows Services, kurz NGWS, umfasst ein<br />

vollständiges Laufzeitsystem für die Ausführung von Code und bietet darüber<br />

hinaus eine Reihe von Diensten, um die Programmierung zu vereinfachen. Sofern<br />

der verwendete Compiler auf die Benutzung dieses Laufzeitsystems eingerichtet<br />

ist, genießen Sie für Ihren Code alle Vorzüge dieser systemseitig verwalteten<br />

Laufzeitumgebung.<br />

Wie wohl auch nicht anders zu erwarten bietet der C#-Compiler Unterstützung<br />

für das NGWS-Laufzeitsystem. Und er ist dabei nicht der einzige; Visual Basic<br />

und C++ sind mit von der Partie. Der Code, den diese Compiler für das NGWS-<br />

Laufzeitsystem generieren, wird als verwalteter Code bezeichnet. Die Vorteile,<br />

die Ihr Code vom NGWS-Laufzeitsystem erhält, sind im Einzelnen:<br />

• Integration über die Sprachgrenzen hinweg (über eine gemeinsame Sprachenspezifikation)<br />

• automatische Speicherverwaltung (Garbage Collector)<br />

• behandlung von Ausnahmezuständen über Sprachgrenzen hinweg<br />

• erhöhte Sicherheit inklusive Typprüfungen<br />

• Unterstützung von Versionsnummern – das Ende des »DLL-Friedhofs«<br />

• vereinfachtes Modell für die Interaktion von Komponenten<br />

Damit das NGWS-Laufzeitsystem diese Unterstützung leisten kann, muss der<br />

Compiler zusammen mit dem verwalteten Code Metadaten erzeugen, die unter<br />

anderem die verwendeten Typen beschreiben und zusammen mit dem Code in der<br />

ausführbaren Datei gespeichert werden. Die Dateien sind wie gewohnt im PE-<br />

Format (»portable executable«) gehalten.


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 29<br />

Einer der Schwerpunkte des NGWS-Laufzeitsystems liegt offensichtlich in der<br />

Integration mehrerer Programmiersprachen. Tatsächlich geht diese Integration so<br />

weit, dass Sie C#-Klassen von Visual-Basic-Objekten ableiten können. (Natürlich<br />

gibt es dafür einige Voraussetzungen, die weiter hinten in diesem Kapitel erläutert<br />

werden.)<br />

Ein Feature, das vor allem das Herz von C/C++-ProgrammiererInnen erfreuen<br />

wird, ist die Speicherverwaltung. Die allseits berüchtigten Speicherlecks sind in<br />

C# kein Thema, weil sich das Laufzeitsystem um alle Objekte kümmert: Sein<br />

Garbage Collector gibt sie automatisch wieder frei, wenn sie nicht länger benötigt<br />

werden (d.h. keine Referenzen mehr auf sie vorhanden sind). Wer jemals das Vergnügen<br />

hatte, sich mit COM-Objekten in C++ herumzuschlagen, wird das besonders<br />

zu schätzen wissen.<br />

Bei der Installation verwalteter Komponenten und Anwendungen auf anderen<br />

Systemen hilft das NGWS-Laufzeitsystem ebenfalls kräftig: Anhand der in den<br />

Dateien gespeicherten Metadaten lässt sich zuverlässig ermitteln, ob Bibliotheken<br />

und andere Dateien in exakt der Version vorliegen, die Ihre Anwendung bzw.<br />

Komponente erwartet. In der Praxis bedeutet das, dass es wesentlich seltener<br />

Probleme mit unpassenden Versionen und nur scheinbar erfüllten Abhängigkeiten<br />

geben wird. Ein weiterer, ebenfalls nicht zu unterschätzender Vorteil der Speicherung<br />

von Metadaten in der ausführbaren Datei: Typinformationen sind am selben<br />

Ort wie der Code – die Registrierung des Systems ist damit als potenzielle Problemquelle<br />

weitgehend ausgeschaltet.<br />

Die beiden nächsten Abschnitte sind den Features des NGWS-Laufzeitsystems<br />

gewidmet, die in erster Linie vor der Ausführung Ihrer C#-Anwendungen zum<br />

Tragen kommen:<br />

• Intermediate Language Code (IL-Code) und Metadaten<br />

• JITter<br />

2.1.1 Intermediate Language Code (IL-Code) und Metadaten<br />

C#-Compiler erzeugen keine direkt ausführbaren Maschinenbefehle, sondern<br />

einen Zwischencode (IL für Intermediate Language), der seinerseits als Input für<br />

einen verwalteten Ausführungsprozess des NGWS-Laufzeitsystems dient. Einer<br />

der großen Vorteile dieses Zwischencodes liegt darin, dass er unabhängig vom<br />

Prozessortyp ist – was aber natürlich auch bedeutet, dass auf dem jeweiligen Zielsystem<br />

ein Compiler existieren muss, der diesen Code in Maschinenbefehle<br />

umsetzt.<br />

Wie erwähnt, beschränkt sich der C#-Compiler nicht auf das Erzeugen von IL-<br />

Code, sondern generiert Metadaten dazu, die dem Laufzeitsystem eine Menge<br />

zusätzlicher Informationen liefern, wie etwa die Definition eines Datentyps und<br />

die Signatur seiner Elementfunktionen. Grob gesagt, enthalten Metadaten alles,


30<br />

Das NGWS-Laufzeitsystem<br />

was Typbibliotheken, Registrierungseinträge und andere Informationen für COM<br />

sind – nur finden sich diese Daten hier eben in derselben Datei wie der IL-Code<br />

(und nicht auf ein halbes Dutzend anderer Speicherorte verteilt).<br />

Das zur Speicherung von IL-Code und Metadaten verwendete Format basiert auf<br />

dem PE-Format, das Win32 seit jeher für EXE- und DLL-Dateien verwendet. Das<br />

NGWS-Laufzeitsystem ist dafür zuständig, beim Laden einer solchen Datei den<br />

IL-Code und die Metadaten auszulesen.<br />

Bevor es nun weiter geht, sollten Sie zumindest eine ungefähre Vorstellung davon<br />

bekommen, woraus IL-Code besteht. Die folgende Liste der Befehlskategorien ist<br />

weder vollständig noch zum Auswendiglernen gedacht, sollte aber einen guten<br />

Überblick darüber geben, auf welche Arten von Anweisungen sich C#-Programme<br />

stützen:<br />

• arithmetische und logische Operationen<br />

• Flusskontrolle<br />

• direkte Speicherzugriffe<br />

• Stackmanipulation<br />

• Argumente und lokale Variablen<br />

• Speicherbelegung auf dem Stack<br />

• Objektmodell<br />

• Werte instanziierbarer Typen (Objekte)<br />

• kritische Abschnitte<br />

• Arrays<br />

• typisierte Speicherorte<br />

2.1.2 JIT-Compiler<br />

Verwalteter Code wird von C#-Compilern – und anderen Compilern, die verwalteten<br />

Code erzeugen können – als Zwischencode (IL-Code) gespeichert. Obwohl<br />

Windows das Ergebnis der Kompilierung als gültige PE-Datei erkennt, ist ein solches<br />

Programm erst nach einer weiteren Umsetzung in Maschinenbefehle ausführbar.<br />

Für diese Umsetzung sind die Just-in-Time-Compiler der NGWS-Laufzeitumgebung<br />

zuständig, die oft auch kurz als JITter bezeichnet werden.<br />

Wieso Just-in-Time – also stückweise bei Bedarf? Warum liest das NGWS-Laufzeitsystem<br />

nicht einfach den gesamten IL-Code einer PE-Datei auf einmal und<br />

setzt ihn in Maschinenbefehle um? Hauptsächlich, weil das bei umfangreicheren<br />

Anwendungen recht lange dauern könnte – und es eben wesentlich effizienter ist,<br />

nur die Teile einer Anwendung umzusetzen, die der Benutzer auch wirklich


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 31<br />

braucht. Anders gesagt: Wer mit einer Textverarbeitung schnell drei Telefonnummern<br />

festhalten will, möchte wohl nur ungern darauf warten, bis das System mit<br />

dem Umsetzen der Grammatikprüfung, des eingebauten Grafikeditors und der<br />

Serienbrieffunktion zu Rande gekommen ist.<br />

Der technische Hintergrund sieht im Wesentlichen so aus: Wenn ein Typ geladen<br />

wird, erzeugt die dafür zuständige Routine des NGWS-Laufzeitsystems für jede<br />

Methode dieses Typs einen kurzen Vorspann (mit Maschinenbefehlen) und setzt<br />

ihn ein. Wenn die Methode zum ersten Mal aufgerufen wird, sorgt dieser Vorspann<br />

für den Aufruf des JIT-Compilers, der seinerseits den IL-Code der Methode<br />

in Maschinenbefehle umsetzt und den Vorspann so ändert, dass er zukünftig auf<br />

die neu erzeugten Maschinenbefehle zeigt. Bei weiteren Aufrufen der Methode<br />

werden die Maschinenbefehle also ohne Umweg ausgeführt (was auch bedeutet,<br />

dass der JIT-Compiler im Verlauf eines Programms irgendwann weitgehend<br />

arbeitslos wird).<br />

Wie der Terminus »JITter« bereits vermuten lässt, gibt es nicht einen einzigen<br />

JIT-Compiler, sondern mehrere davon. Für Windows wird das NGWS-Laufzeitsystem<br />

mit drei verschiedenen JIT-Compilern ausgeliefert:<br />

• JIT ist der Standard-JIT-Compiler des NGWS-Laufzeitsystems: Ein optimierender<br />

Codegenerator mit integrierter Datenflussanalyse, der dicht gepackten,<br />

hoch optimierten, verwalteten Maschinencode erzeugt. JIT kommt mit dem gesamten<br />

IL-Befehlssatz zurecht, hat aber einen entsprechend hohen Ressourcenbedarf<br />

– speziell, was den Speicherplatzverbrauch durch den Compiler selbst,<br />

das sich ergebende Working Set der Anwendung und die für die Optimierungen<br />

notwendige Zeit betrifft.<br />

• EconoJIT ist im Gegensatz zum JIT auf die schnellstmögliche Umsetzung von<br />

IL-Code in Maschinenbefehle ausgelegt und hält das Ergebnis bei Bedarf in einem<br />

(großen) Cache vor. Der erzeugte Maschinencode ist zwangsläufig nicht<br />

so hoch optimiert wie beim JIT, dafür geschieht die Umsetzung aber so schnell,<br />

dass es sich das Laufzeitsystem bei Speicherengpässen leisten kann, momentan<br />

unbenutzten Maschinencode wieder zu verwerfen. Auf diese Weise werden<br />

auch bei Systemen mit magerer Ausstattung des Hauptspeichers Anwendungen<br />

fast beliebiger Größe möglich.<br />

• PreJIT basiert zwar auf JIT, lehnt sich aber wesentlich stärker an traditionelle<br />

Vorbilder an. Dieser Compiler kommt ausschließlich bei der Installation von<br />

NGWS-Komponenten zum Zuge, übersetzt den gesamten IL-Code der Komponente<br />

in verwalteten Maschinencode, und wird im Weiteren nicht mehr benötigt.<br />

Die Hauptvorteile liegen natürlich in geringeren Lade- und Startzeiten<br />

von Anwendungen.<br />

Zwei dieser drei JITter sind zur Laufzeit mit von der Partie. Wie lässt sich festlegen,<br />

welcher JIT wann eingesetzt wird, und welche Strategie er bei der Speicher-


32<br />

Das NGWS-Laufzeitsystem<br />

verwaltung benutzt? Dafür ist der JIT Compiler Manager zuständig – ein kleines<br />

Utility mit dem Dateinamen jitman.exe, das bei der Installation des NGWS-Subsystems<br />

im Ordner bin des SDKs untergebracht wird. Der Start dieses Programms<br />

fügt der Task-Leiste ein Symbol hinzu; ein Doppelklick auf dieses Symbol zeigt<br />

das Dialogfeld dieses Managers an (vgl. Abbildung 2.1).<br />

Bild 2.1:<br />

Der JIT Compiler Manager zur Auswahl und Konfiguration des JIT-<br />

Compilers<br />

So klein dieses Dialogfeld ist, so weit reichend sind die Folgen seiner Einstellungen.<br />

Die folgende Liste gibt einen Überblick:<br />

• Use EconoJIT only – wenn Sie dieses Kästchen leer lassen, verwendet die<br />

NGWS-Laufzeitumgebung den Standardcompiler.<br />

• Max Code Pitch Overhead (%) – gilt ausschließlich für EconoJIT und legt fest,<br />

wieviel Prozent der Laufzeit der Compiler maximal beanspruchen darf. Beim<br />

Überschreiten dieses Schwellwerts wird der Codecache vergrößert (was letztendlich<br />

darauf hinausläuft, dass das Laufzeitsystem seltener bereits übersetzten<br />

Code wieder verwirft und später den Compiler erneut bemühen muss).<br />

• Limit Size of Code Cache – ist standardmäßig nicht gesetzt: Das NGWS-Laufzeitsystem<br />

belegt so viel Hauptspeicher für den Codecache, wie es kriegen<br />

kann. Wenn Sie ein Häkchen in dieses Kästchen setzen, können Sie dem Laufzeitsystem<br />

über Max Size of Cache (bytes) eine Obergrenze für den Codecache<br />

vorschreiben.<br />

• Max Size of Cache (bytes) – legt eine Maximalgröße für den Codecache fest.<br />

Wer will, kann hier recht knauserig sein, sollte aber eine Grenze beachten: Der<br />

Codecache muss in jedem Fall so viel Platz bieten, wie die umfangreichste Methode<br />

einer Anwendung braucht – ansonsten schlägt die JIT-Kompilierung dieser<br />

Methode fehl.<br />

• Optimize For Size – weist den JIT-Compiler an, möglichst Platz sparenden anstelle<br />

von möglichst schnellem Maschinencode zu erzeugen. Dieses Kästchen<br />

trägt standardmäßig kein Häkchen.<br />

• Enable Concurrent GC – ist standardmäßig nicht gesetzt: Der Garbage Collector<br />

läuft in dieser Einstellung im selben Thread wie die jeweilige Anwendung<br />

– und kann diese kurzzeitig blockieren, wenn umfangreichere Aufräumarbei-


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 33<br />

ten zu erledigen sind. Wenn Sie ein Häkchen in dieses Kästchen setzen, spendiert<br />

das NGWS-Laufzeitsystem dem Garbage Collector einen eigenen<br />

Thread. Beachten Sie jedoch, dass der Garbage Collector in einem separaten<br />

Thread etwas langsamer arbeitet und dieses Feature zum Zeitpunkt der Drucklegung<br />

dieses Buchs ausschließlich unter Windows 2000 verfügbar ist.<br />

Da die Beispiele dieses Buchs allesamt recht klein gehalten sind, werden Sie beim<br />

Experimentieren mit den Einstellungen des JIT Compiler Managers kaum große<br />

Unterschiede finden. Die Einstellung des Garbage Collectors macht sich vor<br />

allem bei solchen Anwendungen deutlich bemerkbar, die intensiv von der Benutzeroberfläche<br />

Gebrauch machen.<br />

2.2 Das virtuelle Objektsystem (VOS)<br />

Die vorangehenden Abschnitte haben die Arbeitsweise des NGWS-Laufzeitsystems<br />

beschrieben, ohne dabei auf den technischen Hintergrund näher einzugehen<br />

oder zu erläutern, welche Philosophie dahinter steckt. Die nächsten Seiten holen<br />

diese Erklärungen nach: Sie beschäftigen sich ausschließlich mit dem virtuellen<br />

Objektsystem (VOS).<br />

Das virtuelle Objektsystem legt unter anderem die Regeln fest, denen das NGWS-<br />

Laufzeitsystem bei der Deklaration, dem Einsatz und der Verwaltung von Typen<br />

folgt. Das VOS stellt damit einen Teil eines Rahmenwerks dar, das sowohl die<br />

Integration mehrerer Programmiersprachen als auch die Typensicherheit bei<br />

sprachübergreifender Programmierung gewährleistet, ohne dass die Performance<br />

darunter zu leiden hätte.<br />

Das Rahmenwerk als solches ist die Grundlage der gesamten Architektur des<br />

NGWS-Subsystems. Da das Wissen um diese Architektur beim Entwickeln von<br />

C#-Anwendungen und C#-Komponenten wesentlich ist, gehen die folgenden<br />

Abschnitte auf die vier Teilbereiche dieses Rahmenwerks im Detail ein:<br />

• Das VOS-Typensystem – stellt ein reichhaltiges Typensystem zur Unterstützung<br />

einer breiten Auswahl von Programmiersprachen zur Verfügung.<br />

• Metadaten – beschreiben und referenzieren die vom VOS-Typensystem definierten<br />

Typen. Da das beständige Format von Metadaten unabhängig von der<br />

Programmiersprache ist, lassen sich Metadaten sowohl als Medium zum Datenaustausch<br />

zwischen Werkzeugen als auch für das VES der NGWS (siehe<br />

unten) verwenden.<br />

• Die Common Language Specification (CLS) – definiert eine Untermenge der<br />

vom VOS verwendeten Typen zusammen mit Konventionen für ihren Einsatz.<br />

Wenn eine Klassenbibliothek sich auf diese Typen beschränkt und die Konventionen<br />

einhält, lässt sie sich zusammen mit allen Programmiersprachen verwenden,<br />

die ihrerseits der CLS folgen.


34<br />

Das virtuelle Objektsystem (VOS)<br />

• Das Virtual Execution System (VES) – stellt die Implementation der VOS dar<br />

und ist für das Laden sowie Ausführen von Programmen zuständig, die für das<br />

NGWS-Laufzeitsystem geschrieben worden sind.<br />

2.2.1 Das VOS-Typensystem<br />

Das VOS-Typensystem soll die vollständige Implementation eines breiten Spektrums<br />

an Programmiersprachen unterstützen und muss deshalb nicht nur mit<br />

objektorientierten, sondern auch mit prozeduralen Programmiersprachen zurechtkommen.<br />

Aktuelle Programmiersprachen bieten eine große Zahl weitgehend ähnlicher, aber<br />

dennoch inkompatibler Datentypen. Ein Beispiel dafür sind schlichte Ganzzahlwerte:<br />

VB speichert den Typ Integer mit 16 Bit, C++ den Typ int dagegen mit 32<br />

Bit. Leider lassen sich solche Beispiele angefangen von Typen für Kalenderdatum<br />

und Uhrzeit bis hin zu den Typen für Datenbanken fast beliebig fortsetzen.<br />

Inkompatibilitäten dieser Art machen das Erstellen verteilter Anwendungen<br />

unnötig kompliziert – und das erst recht, wenn mehrere Programmiersprachen im<br />

Spiel sind.<br />

Ein weiteres Problem, das sich aus kleinen Unterschieden der einzelnen Programmiersprachen<br />

ergibt: Ein mit einer Sprache definierter Typ lässt sich nicht in einer<br />

anderen Sprache verwenden. (COM löst dieses Problem mit einem binären Standard<br />

für Schnittstellen, aber auch nur zum Teil). Tatsächlich ist die Wiederverwendung<br />

von Code auch heute noch engen Grenzen unterworfen.<br />

Die größte Hürde für verteilte Anwendungen sind allerdings die Objektmodelle<br />

der einzelnen Programmiersprachen, die sich fast in jedem Punkt unterscheiden –<br />

egal, ob es nun um Ereignisse, Eigenschaften oder die beständige Speicherung<br />

wertbehafteter Elemente geht.<br />

Das VOS-Typensystem stellt einen neuen Ansatz dar, die verschiedenen Programmiersprachen<br />

unter einen Hut zu bringen: Es definiert Typen zur Beschreibung<br />

von Werten und legt sozusagen einen Vertrag fest, an den sich alle Werte eines<br />

bestimmten Typs halten müssen. Da das VOS-Typensystem sowohl objektorientierte<br />

als auch prozedurale Programmiersprachen unterstützt, gibt es hier zwei<br />

Kategorien: Werte und Objekte.<br />

Bei Werten beschreibt der zugeordnete Typ die Speicherrepräsentation sowie die<br />

Operationen, die sich über dieser Repräsentation ausführen lassen. Objekte haben<br />

hier mehr Möglichkeiten, weil der Typ explizit in seiner Repräsentation gespeichert<br />

wird. Jedes Objekt hat eine Identität, die es von allen anderen Objekten<br />

unterscheidet. Details zu den von C# unterstützten VOS-Typen finden Sie in<br />

Kapitel 4, »Die Typen von C#«.


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 35<br />

2.2.2 Metadaten<br />

Obwohl Metadaten primär zur Beschreibung und Referenzierung der durch das<br />

VOS-Typensystem definierten Typen verwendet werden, sind sie nicht auf diesen<br />

Zweck beschränkt. Wenn Sie ein Programm schreiben, werden die von Ihnen<br />

deklarierten Typen – unabhängig davon, ob es dabei um wertbehaftete Typen oder<br />

um Referenzen geht – dem NGWS-Laufzeitsystem über Typdeklarationen<br />

bekannt gemacht, die sich ihrerseits zusammen mit dem Zwischencode in der ausführbaren<br />

Datei befinden.<br />

Metadaten liefern die Informationen, die das NGWS-Laufzeitsystem für verschiedene<br />

Aufgaben benötigt: zum Auffinden und Laden von Klassen, zum Anlegen<br />

von Instanzen (Objekten) dieser Klassen im Hauptspeicher, zum Auflösen von<br />

Methodenaufrufen, zum Umsetzen von IL-Code in Maschinenbefehle, zur Sicherheitsprüfung<br />

und schließlich zum Errichten der kontextbezogenen Grenzen zur<br />

Laufzeit eines Programms (wie etwa Sichtbarkeit, Verfügbarkeit und Lebensdauer<br />

von Objekten).<br />

Wie Metadaten zu Stande kommen, ist für Sie als ProgrammiererIn im Wesentlichen<br />

uninteressant, weil das der C#-Compiler beim Erzeugen des IL-Codes (also<br />

nicht etwa der JIT-Compiler) übernimmt und sie zusammen mit dem IL-Code in<br />

der ausführbaren Datei speichert. Die Art der Speicherung ist selbstverständlich<br />

standardisiert – im Gegensatz zu C++-Compilern, bei denen jede Implementation<br />

ihren eigenen Regeln folgt, wenn es bei exportierten Funktionen um die Kombination<br />

von Signaturen mit Bezeichnern geht.<br />

Der wesentliche Vorteil der Kombination von Metadaten mit Code liegt darin,<br />

dass die Informationen über einen Typ zusammen mit dem Typ selbst gespeichert<br />

sind und sich nicht, wie bisher gewohnt, über mehrere Speicherorte (Typbibliotheken<br />

und die Registrierung) im System verteilen – wovon jeder COM-Programmierer<br />

ein Lied zu singen weiß. Außerdem können Sie im NGWS-Laufzeitsystem<br />

unterschiedliche Versionen einer Bibliothek in ein und demselben Kontext verwenden,<br />

weil diese Bibliotheken eben nicht zentral über die Registrierung referenziert<br />

werden, sondern über die Metadaten in den jeweiligen Dateien.<br />

2.2.3 Die Common Language Specification (CLS)<br />

Diese Spezifikation ist eigentlich kein eigenständiger Teil des virtuellen Objektsystems,<br />

sondern vielmehr eine Spezialisierung. Sie definiert eine Untermenge<br />

der VOS-Datentypen zusammen mit einer Reihe von Konventionen.<br />

Wozu das gut sein soll? Nun: Eine Klassenbibliothek, die sich auf diese Untermenge<br />

beschränkt und an sämtliche von der CLS aufgestellten Regeln hält, lässt<br />

sich von allen Clients verwenden, die ihrerseits der CLS folgen – unabhängig<br />

davon, mit welcher Sprache die einzelnen Klassen definiert wurden. Dabei geht


36<br />

Das virtuelle Objektsystem (VOS)<br />

es erfreulicherweise nicht um den kleinsten gemeinsamen Nenner: Die CLS<br />

schränkt ausschließlich die nach außen hin sichtbaren Elemente von Klassen ein<br />

(wie Methoden, Eigenschaften und Ereignisse), nicht aber deren Implementation.<br />

Das Ergebnis: Niemand hält Sie davon ab, eine Komponente in C# zu schreiben,<br />

von dieser Klasse mit VB eine weitere Klasse abzuleiten – und weil Ihnen die mit<br />

VB hinzugefügte Funktionalität so gut gefällt, können Sie diese Klasse dann<br />

erneut als Basis einer weiteren Ableitung in C# verwenden. Das funktioniert tatsächlich<br />

– solange sich die nach außen hin sichtbaren Elemente dieser Klasse an<br />

die von der CLS aufgestellten Regeln halten.<br />

Die in diesem Buch vorgestellten Beispiele schenken diesen Regeln wenig<br />

Beachtung - nicht zuletzt, weil es dabei in den allermeisten Fällen um eigenständige<br />

Programme und nicht um Komponenten geht. Wenn Sie eine Klassenbibliothek<br />

aufbauen wollen, sieht die Sache etwas anders aus. Tabelle 2.1 gibt einen<br />

Überblick über die Typen und Konventionen der CLS (die, wie gesagt, ausschließlich<br />

für nach außen hin sichtbare Elemente von Klassen gelten).<br />

Vollständig ist diese Liste nicht – sie beschränkt sich auf die wesentlichen Punkte.<br />

Da in den weiteren Kapiteln auch nicht bei jedem einzelnen Typ extra auf die<br />

CLS-Verträglichkeit eingegangen wird, sollten Sie sie aber wenigstens überfliegen<br />

(und sich bitte nicht wundern, weil einige der verwendeten Termini erst in<br />

den hinteren Kapiteln dieses Buchs erklärt werden).<br />

Einfache Typen<br />

bool<br />

char<br />

byte<br />

short<br />

int<br />

long<br />

float<br />

double<br />

string<br />

object (die Basisklasse aller Objekte)<br />

Arrays<br />

Die Dimension muss bekannt sein (>=1) und die Zählung der Elemente grundsätzlich<br />

mit 0 beginnen. Die Elemente müssen einen CLS-Typ haben.<br />

Tab. 2.1:<br />

Typen und Konventionen der Common Language Specification


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 37<br />

Typen<br />

– können abstrakt oder versiegelt sein.<br />

– können eine beliebige Zahl von Schnittstellen implementieren. Methoden mit gleichen<br />

Namen und gleicher Signatur als Teil verschiedener Schnittstellen sind möglich.<br />

– Klassen müssen von (exakt) einer anderen Klasse abgeleitet sein. Das Verbergen und<br />

Überschreiben von Elementen ist erlaubt.<br />

– können eine beliebige Zahl von Elementen (wertbehaftet, Methoden, Ereignisse,<br />

Objekte) haben.<br />

– können eine beliebige Zahl von Konstruktoren definieren.<br />

– können wahlweise öffentlich oder nur innerhalb der jeweiligen NGWS-Komponente<br />

verfügbar sein. Nur die öffentlich verfügbaren Elemente werden als Teil der Schnittstelle<br />

gewertet und müssen sich an die CLS halten.<br />

– Alle wertbehafteten Typen müssen von System.ValueType abgeleitet sein. Die Ausnahme<br />

von dieser Regel sind Aufzählungen, bei denen als Basisklasse System.Enum<br />

vorgeschrieben ist.<br />

Elemente<br />

können Elemente einer Basisklasse wahlweise überschreiben und verbergen.<br />

Argument- und Ergebnistypen von Methoden sind auf die von der CLS definierten<br />

Typen beschränkt.<br />

Konstruktoren, Methoden und Eigenschaften können überschrieben werden.<br />

Typen können abstrakte Elemente enthalten, dürfen dann aber nicht versiegelt sein.<br />

Methoden<br />

Können wahlweise statisch, virtuell oder auf die jeweilige Instanz bezogen sein.<br />

Virtuelle und auf die jeweilige Instanz einer Klasse bezogene Methoden können wahlweise<br />

abstrakt sein oder eine Implementation haben. Für statische Methoden ist dagegen<br />

eine Implementation gefordert.<br />

Virtuelle Methoden können (müssen aber nicht) endgültig sein.<br />

Wertbehaftete Elemente<br />

können statisch oder auf die jeweilige Instanz bezogen sein.<br />

Statische Elemente können entweder Literale sein oder müssen im Konstruktor gesetzt<br />

werden.<br />

Eigenschaften<br />

lassen sich wahlweise über get- und set-Methoden oder über die für Eigenschaften vorgesehene<br />

Syntax zur Verfügung stellen.<br />

Der Ergebnistyp der get-Methode und der erste Parameter der set-Methode müssen<br />

denselben CLS-Typ haben – nämlich den Typ der Eigenschaft.<br />

Tab. 2.1:<br />

Typen und Konventionen der Common Language Specification (Forts.)


38<br />

Das virtuelle Objektsystem (VOS)<br />

Eigenschaften einer Klasse müssen sich in ihren Namen unterscheiden. Unterschiedliche<br />

Typen sind hier nicht ausreichend.<br />

Da der Zugriff auf Eigenschaften über Methoden stattfindet, gilt: Wenn eine Klasse eine<br />

Eigenschaft mit dem Namen Eigenschaft definiert, sind die Bezeichner get_Eigenschaft<br />

und set_Eigenschaft innerhalb dieser Klasse für die Zugriffsfunktionen auf<br />

diese Eigenschaft reserviert.<br />

Eigenschaften lassen sich indizieren.<br />

Zugriffsfunktionen für Eigenschaften müssen dem Namensschema get_Eigenschaft,<br />

set_Eigenschaft folgen.<br />

Aufzählungen<br />

Als zugrunde liegende Typen sind ausschließlich byte, short, int und long verwendbar.<br />

Die Elemente eines Aufzählungstyps sind Konstanten, die den der Aufzählung zugrunde<br />

liegenden Typ tragen.<br />

Aufzählungen können keine Schnittstellen implementieren.<br />

Mehrere Elemente einer Aufzählung können identische Werte haben.<br />

Aufzählungen müssen von System.Enum abgeleitet sein (wofür der C#-Compiler implizit<br />

sorgt).<br />

Ausnahmen<br />

lassen sich signalisieren und abfangen.<br />

Eigene Klassen für Ausnahmen müssen von System.Exception abgeleitet sein.<br />

Schnittstellen<br />

können die Implementation anderer Schnittstellen voraussetzen.<br />

können Eigenschaften, Ereignisse und virtuelle Methoden definieren. Die Implementation<br />

dieser Elemente ist Sache der abgeleiteten Klasse.<br />

Ereignisse<br />

müssen entweder keine oder beide Methoden zum An- und Abmelden von Clients definieren.<br />

Beide Methoden erwarten einen Parameter, nämlich eine Referenz auf eine von<br />

System.Delegate abgeleitete Klasse.<br />

Für die Methoden ist das folgende Namensschema obligatorisch: add_Ereignisname,<br />

remove_Ereignisname, raise_Ereignisname.<br />

Selbstdefinierte Attribute<br />

Hier sind ausschließlich die folgenden Typen verwendbar: string, char, bool, byte,<br />

short, int, long, float, double, enum (eines CLS-verträglichen Typs) und object.<br />

Delegationen<br />

lassen sich anlegen und aufrufen.<br />

Tab. 2.1:<br />

Typen und Konventionen der Common Language Specification (Forts.)


Kapitel 2 • Der Unterbau – das NGWS-Laufzeitsystem 39<br />

Bezeichner<br />

Für das erste Zeichen eines Bezeichners gibt es einige Einschränkungen.<br />

Zwischen Groß- und Kleinschreibung wird nicht unterschieden. Zwei Bezeichner innerhalb<br />

eines Geltungsbereichs, die sich lediglich durch Groß- und Kleinschreibung voneinander<br />

unterscheiden, sind deshalb nicht zulässig.<br />

Tab. 2.1:<br />

Typen und Konventionen der Common Language Specification (Forts.)<br />

2.2.4 Das Virtual Execution System (VES)<br />

Das Virtual Execution System implementiert das virtuelle Objektsystem. Es<br />

besteht im Wesentlichen aus einer Execution Engine (EE), die ihrerseits für das<br />

NGWS-Laufzeitsystem verantwortlich ist. Die EE führt die Anwendungen aus,<br />

die in C# geschrieben und kompiliert wurden.<br />

Das VES umfasst:<br />

• die Intermediate Language (IL) - eine Zwischensprache, die vom Design her<br />

ein breites Spektrum von Compilern abdeckt. Im Lieferumfang der NGWS-<br />

Implementation für Windows sind Compiler für C++, Visual Basic und C# enthalten,<br />

die IL generieren können.<br />

• das Laden von verwaltetem Code - dazu gehört das Auflösen von Namen, das<br />

Anlegen von Objektvariablen im Hauptspeicher sowie das Einsetzen eines<br />

Vorspanns (aus Maschinenbefehlen) in die Methoden zum Aufruf des JIT-<br />

Compilers. Die für Klassen zuständige Laderoutine kümmert sich des Weiteren<br />

um die Sicherheit: Sie übernimmt Konsistenzprüfungen und sorgt für die<br />

Einhaltung der im Rahmen der Typdefinition aufgestellten Verfügbarkeitsregeln.<br />

• die Umwandlung von IL-Code in Maschinenbefehle per JIT - IL-Code ist weder<br />

mit dem traditionellen Bytecode für Interpreter noch mit Tree-Code vergleichbar.<br />

Die Umwandlung in Maschinencode stellt eine echte Kompilierung<br />

dar.<br />

• das Laden von Metadaten, Prüfung der Typen und Prüfung der Integrität von<br />

Methoden.<br />

• Garbage Collection und Ausnahmebehandlung - beide Dienste basieren auf der<br />

Interpretation des Stacks, dessen Aufbau sich bei verwaltetem Code schrittweise<br />

verfolgen lässt. Damit das Laufzeitsystem die einzelnen Stackbereiche<br />

(»Frames«) auseinander halten kann, muss ein Code-Manager vorhanden sein,<br />

der entweder vom Compiler oder vom JIT gestellt wird.<br />

• Profiling und Debugging – diese beiden Dienste sind von den Informationen<br />

abhängig, die der C#-Compiler zur Verfügung stellt. Hier geht es um zwei Tabellen:<br />

eine für die Zuordnung von Quelltextzeilen zu Adressen im IL-Code


40<br />

Zusammenfassung<br />

und eine für die Zuordnung von Code zu Adressen auf dem Stack. Beide Tabellen<br />

müssen während der Umwandlung von IL-Code in Maschinenbefehle<br />

neu berechnet werden.<br />

• die Verwaltung von Threads und Kontexten sowie Fernzugriffen - diese Dienste<br />

stellt das VES verwaltetem Code ebenfalls zur Verfügung.<br />

Auch wenn diese Liste nicht vollständig ist, sollte sie doch einen guten Eindruck<br />

davon vermitteln, wie das NGWS-Laufzeitsystem durch die Infrastruktur des<br />

VES unterstützt wird. Es steht mit Sicherheit zu erwarten, dass über das NGWS<br />

noch komplette Bücher geschrieben werden, die sich dann auch mit den einzelnen<br />

Teilen intensiver beschäftigen.<br />

2.3 Zusammenfassung<br />

Dieses Kapitel hat Ihnen das NGWS-Laufzeitsystem in einer Art Rundgang präsentiert<br />

und seine Arbeitsweise beim Erstellen, Kompilieren und der Verbreitung<br />

von C#-Programmen vorgestellt. C#-Compiler erzeugen keine Maschinenbefehle,<br />

sondern IL-Code, der gemeinsam mit Metadaten zur Beschreibung der verwendeten<br />

Typen in einer ausführbaren Datei gespeichert wird. Ein JIT-Compiler extrahiert<br />

den IL-Code und setzt ihn mit Hilfe der Metadaten in Maschinenbefehle um.<br />

Mit dem JIT Compiler Manager können Sie festlegen, welcher der drei JIT-Compiler<br />

des NGWS-Laufzeitsystems zum Einsatz kommen soll.<br />

Im zweiten Teil dieses Kapitels ging es um die hinter dem Laufzeitsystem stehende<br />

Theorie: das Virtuelle Objektsystem (VOS) und seine Teile. Für das Design<br />

von Klassenbibliotheken besonders interessant ist die Common Language Specification<br />

(CLS), die Regeln zur sprachübergreifenden Nutzung von Klassen festlegt.<br />

Schließlich folgten noch einige Details des Virtual Execution Systems<br />

(VES), das eine Implementation des VOS durch das NGWS-Laufzeitsystem darstellt.


Kapitel 3<br />

Die erste C#-Anwendung<br />

3.1 Wahl eines Editors 42<br />

3.2 Hello World 43<br />

3.3 Kompilieren der Anwendung 45<br />

3.4 Ein- und Ausgabe 46<br />

3.5 Kommentare 48<br />

3.6 Zusammenfassung 50


42<br />

Wahl eines Editors<br />

Wie für Programmierbücher üblich, wird auch dieses Buch die ersten Gehversuche<br />

mit der neuen Programmiersprache C# in Form eines »Hello World«-Programms<br />

unternehmen. Das Kapitel selbst ist kurz, es dient eigentlich nur dem<br />

Zweck, Ihnen die grundlegenden Bestandteile einer C#-Anwendung vorzustellen<br />

und einen ersten Eindruck davon zu vermitteln, wie man in C# eine Anwendung<br />

schreibt, kompiliert und einfache Ein- und Ausgaben bewerkstelligt.<br />

3.1 Wahl eines Editors<br />

Nicht einmal ein bekennender Notepad-Enthusiast, wie der Autor dieses Buchs,<br />

wird Ihnen diesen Editor für die Bearbeitung von C#-Quellcode empfehlen. Das<br />

liegt schlicht und einfach daran, dass es sich bei C# um eine waschechte Compilersprache<br />

handelt, deren Compiler mit Fehlermeldungen nicht gerade geizig<br />

umgeht – wer von C/C++ kommt, weiß, was das heißt.<br />

Als gute Wahl kommen eine ganze Reihe von Editoren in Frage. So können Sie<br />

Ihr gutes altes Visual C++ 6.0 so umkonfigurieren, dass es mit C#-Quelldateien<br />

zurechtkommt, oder Sie arbeiten gleich mit dem neuen Visual Studio 7. Weiterhin<br />

können Sie natürlich auch jeden anderen Editor benutzen, der Zeilennummern<br />

kennt, Code farbig unterlegen, Werkzeuge (den Compiler) aufrufen kann und eine<br />

halbwegs brauchbare Suchfunktion zu bieten hat. Ein gutes Beispiel für einen solchen<br />

Editor ist CodeWright, dessen Arbeitsbereich in Abbildung 3.1 zu sehen ist.<br />

Bild 3.1:<br />

CodeWright, einer der vielen Editoren, die eine gute Wahl für die<br />

Bearbeitung von C#-Quellcode darstellen


Kapitel 3 • Die erste C#-Anwendung 43<br />

Es versteht sich, dass keiner der erwähnten Editoren wirklich notwendig ist, um<br />

ein C#-Programm zu schreiben – im Prinzip kommt man auch mit Notepad aus.<br />

Falls Sie aber planen, ernsthafter mit C# zu programmieren, sollten Sie sich für<br />

eine komfortablere Alternative entscheiden.<br />

3.2 Hello World<br />

Nach diesem kurzen Ausflug in die Welt der Editoren zurück zu »Hello World«,<br />

der berühmtesten aller kleinen Anwendungen. Listing 3.1 zeigt die kürzeste C#-<br />

Formulierung für dieses Programm. Speichern Sie den Code in einer Datei<br />

namens helloworld.cs, damit Sie ihn später kompilieren können.<br />

Listing 3.1:<br />

Das Programm »Hello World« in seiner einfachsten Gestalt<br />

1: class HelloWorld<br />

2: {<br />

3: public static void Main()<br />

4: {<br />

5: System.Console.WriteLine("Hello World");<br />

6: }<br />

7: }<br />

Der C#-Compiler erwartet, dass Sie Codeblöcke (Anweisungen) in geschweifte<br />

Klammern ({ und }) setzen. Selbst wer noch nie ein C#- oder C++-Programm<br />

gesehen hat, müsste somit erkennen, dass die Methode Main der Anweisung class<br />

untergeordnet ist, weil sie in deren Codeblock definiert ist.<br />

Der Startpunkt einer jeden (als eigenständiges Programm ausführbaren) C#-<br />

Anwendung ist die statische Methode Main, die in einer der Klassen des Programms<br />

definiert sein muss. Umgekehrt darf diese Methode immer nur von einer<br />

Klasse gestellt werden, es sei denn, Sie teilen dem Compiler explizit mit, welche<br />

Main-Methode er als Startpunkt verwenden soll (im Fehlerfall macht sich der<br />

Compiler natürlich durch eine entsprechende Meldung bemerkbar).<br />

Im Gegensatz zu C++ beginnt Main in C# mit einem großgeschriebenen »M«. Mit<br />

dem Codeblock dieser Methode startet und endet Ihr Programm. Natürlich können<br />

Sie von diesem Codeblock aus weitere Methoden aufrufen, wie im Beispiel<br />

die statische Methode WriteLine, oder Objekte anlegen und deren Methoden ausführen.<br />

Als Ergebnistyp für Main ist der Datentyp void vereinbart:<br />

public static void Main()<br />

Obwohl C++-ProgrammiererInnen die äußere Gestalt solcher Anweisungen bestens<br />

vertraut sein dürfte, benötigen diejenigen, die von einer anderen Programmiersprache<br />

kommen, wahrscheinlich noch eine Erläuterung der beiden Modifi-


44<br />

Hello World<br />

zierer. public erklärt die Methode als »öffentlich«, so dass sie von jeder anderen<br />

Methode aus aufrufbar ist (und damit auch von der Laderoutine), gleich ob diese<br />

derselben Klasse angehört oder nicht. Ergänzend besagt static, dass die Methode<br />

zur Ausstattung der Klasse selbst gehört und für ihren Aufruf nicht eigens eine<br />

Instanz der Klasse angelegt werden muss. Anstelle eines Objektbezeichners kann<br />

somit auch der Klassenbezeichner stehen:<br />

HelloWorld.Main();<br />

Es ist natürlich alles andere als klug, diese Anweisung in die Methode Main zu<br />

setzen: Die unvermeidliche Folge wäre eine endlose Rekursion – und somit ein<br />

Stacküberlauf.<br />

Ein weiterer wichtiger Aspekt ist der Ergebnistyp. Für die Methode Main haben<br />

Sie die Wahl zwischen void und int. Bei Deklaration mit void gibt die Methode<br />

keinen Wert zurück, bei Deklaration mit int eine Ganzzahl, die im Allgemeinen<br />

den Fehlerstatus beim Beenden der Anwendung ausdrückt. Damit lässt sich Main<br />

insgesamt auf zwei verschiedene Arten deklarieren:<br />

public static void Main()<br />

public static int Main()<br />

Als C++-ProgrammiererIn werden Sie wahrscheinlich bereits ahnen, was als<br />

Nächstes kommt: Ja, man kann einer Anwendung Kommandozeilenparameter in<br />

Form eines Arrays übergeben. Die Deklaration sieht dann so aus:<br />

public static void Main(string[] args)<br />

Hier ist nicht die richtige Stelle, um den Zugriff auf diese Werte im Einzelnen zu<br />

diskutieren. Soviel sei aber gesagt: Als C++-ProgrammiererIn wird es Sie wahrscheinlich<br />

überraschen, dass C# den Aufrufpfad der Anwendung im Gegensatz zu<br />

C++ nicht in diesem Array übergibt, sondern sich wirklich nur auf die Kommandozeilenparameter<br />

beschränkt.<br />

Nach dieser nun doch nicht so kurz gewordenen Einführung in die formalen<br />

Besonderheiten der Methode Main, ein Blick auf die einzige wirklich substanzielle<br />

Codezeile, die letztlich die Meldung »Hello World« auf den Bildschirm<br />

schreibt:<br />

System.Console.WriteLine("Hello World");<br />

Wäre da nicht der Qualifizierer System, würde man sofort darauf kommen, dass<br />

WriteLine eine statische Methode der Klasse Console verkörpert. Wofür steht<br />

denn System? Nun, System ist der Namensraum, in dem die Klasse Console definiert<br />

ist. Da es unpraktisch ist, diesen Bezeichner für jeden Zugriff auf Console zu<br />

notieren, sieht C# die Möglichkeit vor, einen Namensraum auch als Ganzes zu<br />

importieren. Listing 3.2 zeigt, wie der Import vor sich geht:


Kapitel 3 • Die erste C#-Anwendung 45<br />

Listing 3.2:<br />

Einen Namensraum in die Anwendung importieren<br />

1: using System;<br />

2:<br />

3: class HelloWorld<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: Console.WriteLine("Hello World");<br />

8: }<br />

9: }<br />

Wie Sie sehen, umfasst die Importdeklaration nichts weiter als die Direktive<br />

using und den Bezeichner des zu importierenden Namensraums. Von nun an lassen<br />

sich die Elemente aus diesem Namensraum auch ohne explizite Qualifikation<br />

notieren. Aus dem riesigen Pool an Namensräumen, den das NGWS-Subsystem<br />

bereitstellt, werden Sie im Verlauf dieses Buchs nur einige wenige Elemente konkret<br />

kennenlernen. Kapitel 8, »Komponenten mit C# schreiben«, wird Sie allerdings<br />

mit dem nötigen Handwerkzeug zur Definition eigener Namensräume ausrüsten.<br />

3.3 Kompilieren der Anwendung<br />

Da zum Installationsumfang des NGWS-Laufzeitsystems auch alle notwendigen<br />

Compiler (VB, C++ und C#) gehören, sind Sie vom Prinzip her nicht auf den<br />

Kauf eines weiteren Entwicklungswerkzeugs angewiesen, um Ihre C#-Programme<br />

in die Sprachebene der IL (Intermediate Language) zu übersetzen. Sollten<br />

Sie noch nie eine Anwendung mit einem kommandozeilenorientierten Compiler<br />

übersetzt haben (und Make-Dateien zwar zu Ihrem Vokabular, nicht jedoch zu<br />

Ihren sinnlichen Erfahrungen zählen), werden Sie nun Ihr Debüt feiern.<br />

Öffnen Sie ein Fenster mit der MS-DOS-Eingabeaufforderung und wechseln Sie<br />

in das Verzeichnis, das die zuvor gespeicherte Datei helloworld.cs enthält. Dann<br />

tippen Sie das folgende Kommando:<br />

csc helloworld.cs<br />

Es bewirkt, dass helloworld.cs kompiliert und zu der ausführbaren Datei helloworld.exe<br />

gebunden wird. Nachdem der Code fehlerfrei ist (sofern Sie ihn richtig<br />

abgetippt haben), bewältigt der C#-Compiler den Übersetzungsvorgang, wie in<br />

Abbildung 3.2 gezeigt, ohne das geringste Murren.<br />

Nun ist es soweit. Sie können Ihre erste C#-Anwendung starten: Sobald Sie den<br />

Befehl helloworld in die Kommandozeile eingegeben haben, müsste die Ausgabe<br />

Hello World am Bildschirm erscheinen.


46<br />

Ein- und Ausgabe<br />

Bild 3.2:<br />

Die Anwendung mit dem Kommandozeilen-Compiler übersetzen<br />

Bevor es weitergeht: Probieren Sie doch noch den folgenden Compilerschalter<br />

aus:<br />

csc /out:hello.exe helloworld.cs<br />

Der Schalter ermöglicht es Ihnen, den Namen der ausführbaren Datei frei zu wählen.<br />

Dieser wahrlich unscheinbare Zusatz wird sich für den weiteren Einsatz des<br />

Compilers für die Codebeispiele in diesem Buch noch als recht wertvoll erweisen.<br />

3.4 Ein- und Ausgabe<br />

Was die bisherige Diskussion betrifft, so haben Sie an Code nichts weiter als die<br />

Ausgabe einer literalen Zeichenfolge über die Konsole zu sehen bekommen.<br />

Obwohl dieses Buch die Konzepte der C#-Programmierung darzustellen versucht,<br />

und nicht die Programmierung mit Benutzerschnittstellen, werden Sie nun erst<br />

einmal eine Handvoll einfacher Methoden kennenlernen, die Ihnen die Abwicklung<br />

von Ein- und Ausgaben über die Konsole gestatten – nämlich die Äquivalente<br />

der C-Funktionen scanf und printf bzw. der C++-Funktionen cin und cout.<br />

(VB ist vom Kern her nicht auf die konsolenorientierte Programmierung ausgelegt,<br />

weshalb sich hier auch keine Äquivalente angeben lassen.)<br />

Um Ihr Programm mit der Außenwelt in Kontakt treten zu lassen, genügt es vom<br />

Prinzip her, wenn es Ausgaben produzieren und Eingaben des Benutzers entgegennehmen<br />

kann. Listing 3.3 zeigt, wie man den Benutzer zur Eingabe seines<br />

Namens veranlasst, den Namen einliest und als Teil einer persönlichen Begrüßungsformel<br />

wieder ausgibt.


Kapitel 3 • Die erste C#-Anwendung 47<br />

Listing 3.3:<br />

Eine Eingabe von der Konsole lesen<br />

1: using System;<br />

2:<br />

3: class InputOutput<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: Console.Write("Bitte geben Sie Ihren Namen ein: ");<br />

8: string strName = Console.ReadLine();<br />

9: Console.WriteLine("Hallo " + strName);<br />

10: }<br />

11: }<br />

Zeile 7 enthält den Aufruf von Write, einer weiteren statischen Methode von Console,<br />

die für Textausgaben über die Konsole zuständig ist. Im Unterschied zu WriteLine<br />

verzichtet Write auf einen Zeilenvorschub nach Ausgabe des letzten Zeichens.<br />

Die Ausgabe mit Write bewirkt hier also, dass der Benutzer seinen Namen<br />

im Anschluss an die Eingabeaufforderung, d.h. in die gleiche Zeile, eingibt.<br />

Sobald der Benutzer seinen Namen eingegeben und die Eingabetaste betätigt hat,<br />

liest das Programm die Eingabe mit der Methode ReadLine in die String-Variable<br />

strName und gibt deren Wert verkettet mit dem Literal "Hallo " mit der bereits<br />

bekannten Methode WriteLine wieder über die Konsole aus (vgl. Abbildung 3.3).<br />

Bild 3.3:<br />

Die veränderte Version der Anwendung »Hello« übersetzen und<br />

ausführen


48<br />

Kommentare<br />

Das wäre es fast schon gewesen. Es fehlt eigentlich nur noch, dass Sie dem<br />

Benutzer mehrere Werte als formatierte Ausgabe präsentieren können. Listing 3.4<br />

zeigt ein Beispiel:<br />

Listing 3.4:<br />

Formatierte Ausgaben<br />

1: using System;<br />

2:<br />

3: class InputOutput<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: Console.Write("Bitte geben Sie Ihren Namen ein: ");<br />

8: string strName = Console.ReadLine();<br />

9: Console.WriteLine("Hallo {0}",strName);<br />

10: }<br />

11: }<br />

Zeile 9 enthält eine WriteLine-Anweisung, deren erster Parameter eine Formatbeschreibung<br />

darstellt und deren zweiter Parameter den in dieser Formatierung auszugebenden<br />

Wert liefert. Die Formatbeschreibung lautet:<br />

"Hallo {0}"<br />

Die Zeichenfolge {0} fungiert in dieser Beschreibung als Platzhalter für den Wert<br />

des zweiten Parameters. WriteLine lässt in Erweiterung dieses Schemas bis zu<br />

drei Parameter zu, die sich in der Formatbeschreibung über Platzhalter repräsentieren<br />

lassen:<br />

Console.WriteLine("Hallo {0} {1} aus {2}", sVorname, sNachname, sStadt);<br />

Es dürfte Sie nicht überraschen, dass man hier nicht nur Zeichenfolgen, sondern<br />

auch Werte anderer Typen einsetzen kann – Typen werden im Kapitel 4, »Die<br />

Typen von C#«, noch ausführlicher vorgestellt.<br />

3.5 Kommentare<br />

Ein wichtiger Bestandteil von Quelltexten sind Kommentare. Sie informieren<br />

gewissermaßen »vor Ort« über spezielle Details der Implementation, durchgeführte<br />

Änderungen und so weiter. Grundsätzlich bleibt es natürlich Ihnen selbst<br />

überlassen, ob Sie Kommentare benutzen und was Sie in einen Kommentar hineinschreiben,<br />

Sie müssen sich nur an die von C# vorgeschriebene Syntax halten,<br />

wenn Sie Kommentare setzen. Listing 3.5 zeigt die beiden Möglichkeiten, in C#<br />

Kommentare zu platzieren:


Kapitel 3 • Die erste C#-Anwendung 49<br />

Listing 3.5:<br />

Kommentare in einen Quelltext einfügen<br />

1: using System;<br />

2:<br />

3: class HelloWorld<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: // das ist ein Kommentar in Zeilensyntax<br />

8: /* dieser Kommentar ist in Blocksyntax gehalten<br />

9: und kann auch mehrere Zeilen umfassen */<br />

10: Console.WriteLine(/*"Hello World"*/);<br />

11: }<br />

12: }<br />

In der Zeilensyntax sorgt der Kommentaroperator // dafür, dass der Compiler den<br />

Rest der jeweiligen Zeile als Kommentar betrachtet:<br />

int nMyVar = 10; // blah blah<br />

Beachten Sie, dass Sie mit dieser Syntax Zeilen entweder ganz oder ab einer<br />

gewissen Spalte bis zum Zeilenende als Kommentar ausblenden können. Dasselbe<br />

Konzept findet sich auch in C++ und in Visual Basic (hier wird der Kommentar<br />

allerdings durch ein Hochkomma eingeleitet). Falls Sie einen mehrzeiligen<br />

Kommentar einfügen oder einen beliebigen Block auskommentieren wollen,<br />

verwenden Sie besser die Blocksyntax mit den beiden Kommentaroperatoren /*<br />

und */. Der Compiler ignoriert in diesem Fall alles, was zwischen diesen beiden<br />

Operatoren steht. Exakt dieselbe Blocksyntax für Kommentare ist auch in C und<br />

C++ (nicht jedoch in VB) zulässig, so dass Sie in allen drei Programmiersprachen<br />

(C, C++ und C#) in dieselbe Falle tappen können. Angenommen, Sie haben in<br />

einer beliebigen Codesequenz bereits einen Block auskommentiert:<br />

Console.WriteLine(/*"Hello World"*/);<br />

und wollen eben mal schnell zu Testzwecken einen weiteren größeren Block auskommentieren,<br />

in dem jedoch der erste Block enthalten ist:<br />

/*<br />

...<br />

Console.WriteLine(/*"Hello World"*/);<br />

...<br />

*/<br />

Wenn Sie den größeren Block nun einfach durch die beiden Kommentaroperatoren<br />

kennzeichnen, entsteht das Problem, dass der Compiler nach Eintritt in den<br />

Kommentarblock den nächsten schließenden Kommentaroperator – also den des<br />

eingeschlossen Kommentarblocks – als Blockende betrachtet. Damit wird der<br />

restliche Code bis zum eigentlichen schließenden Kommentaroperator für den


50<br />

Zusammenfassung<br />

Compiler sichtbar und kann ihm eine Reihe recht interessanter Fehlermeldungen<br />

abtrotzen – zumindest jedoch eine Meldung über den vermeintlich ungepaarten<br />

Operator */. Seien Sie also auf der Hut.<br />

3.6 Zusammenfassung<br />

In diesem Kapitel haben Sie Ihre erste C#-Anwendung kompiliert und gestartet:<br />

die Anwendung »Hello World«. Anhand dieser zu einigem Ruhm gekommenen<br />

kleinen Anwendung haben Sie die Methode Main kennengelernt, den definierten<br />

Startpunkt – und Endpunkt – eines jeden C#-Programms. Main kann so deklariert<br />

sein, dass diese Methode entweder keinen Ergebniswert zurückliefert oder aber<br />

eine ganzzahlige Fehlernummer. Zudem lässt sich bei Bedarf ein Array des Typs<br />

string als Aufrufparameter deklarieren, so dass sich mit dieser Methode auch<br />

Kommandozeilenparameter auswerten lassen.<br />

Ausgehend von dieser Anwendung haben Sie noch einige Ein- und Ausgabemethoden<br />

der Klasse Console kennengelernt. Sie ermöglichen Ihnen, halbwegs vernünftige<br />

Beispielprogramme für die Konsole zu schreiben, wenn Sie die Sprache<br />

C# erlernen. Später werden Sie als Benutzerschnittstelle natürlich WFC, Win-<br />

Forms oder ASP+ verwenden.


Kapitel 4<br />

Die Typen von C#<br />

4.1 Wertbehaftete Typen 52<br />

4.2 Referenztypen 57<br />

4.3 Boxing und Unboxing 62<br />

4.4 Zusammenfassung 64


52<br />

Wertbehaftete Typen<br />

Nachdem Sie nun wissen, wie man ein einfaches Programm in C# schreibt, stelle<br />

ich Ihnen nun das Typensystem von C# vor. Dieses Kapitel zeigt den Einsatz der<br />

verschiedenen Wert- und Referenztypen und erläutert, was der als Boxing und<br />

Unboxing bezeichnete Mechanismus für Sie tun kann. Allzu viele Praxisbeispiele<br />

werden Sie auf den folgenden Seiten nicht finden – dafür aber eine Menge allgemeiner<br />

Informationen darüber, wie man in Programmen aus diesen Typen das<br />

Beste herausholt.<br />

4.1 Wertbehaftete Typen<br />

Eine Variable, die einen wertbehafteten Typ trägt, speichert einen konkreten Wert.<br />

Weil der Compiler jeden Einsatz uninitialisierter Variablen in Berechnungen als<br />

fehlerhaft moniert – Sie also zwingt, Variablen vor ihrer Verwendung zu initialisieren<br />

–, kann das von anderen Programmiersprachen her gewohnte Problem<br />

nicht initialisierter Variablen in C# per se nicht auftreten.<br />

Die Wertzuweisung an eine Variable mit wertbehaftetem Typ ist daher ein Kopiervorgang,<br />

bei dem der zugewiesene Wert an sich kopiert wird. Bei der Zuweisung<br />

eines Referenztyps wird dagegen nur eine weitere Referenz auf ein und denselben<br />

Wert generiert. Der betroffene Wert verbleibt an Ort und Stelle und kann nun<br />

sowohl über die ursprüngliche als auch über die neue Referenz angesprochen<br />

werden – es gibt ja zwei Variablen, die auf ihn referieren.<br />

Die wertbehafteten Typen von C# lassen sich in drei Kategorien unterteilen:<br />

• einfache Typen<br />

• strukturierte Typen (struct)<br />

• Aufzählungen<br />

4.1.1 Einfache Typen<br />

Die von C# zur Verfügung gestellten einfachen Typen haben einige gemeinsame<br />

Charakteristika. Zum einen handelt es sich bei ihnen ausnahmslos um Aliase der<br />

vom NGWS-Subsystem unterstützten Typen, zum zweiten werden konstante Ausdrücke,<br />

die aus Werten mit einfachen Typen zusammengesetzt sind, bereits während<br />

der Compilierung ausgewertet (und nicht erst zur Laufzeit). Des Weiteren<br />

lassen sich alle einfachen Typen mit Literalen initialisieren.<br />

Die einfachen Typen von C# lassen sich folgendermaßen gruppieren:<br />

• Ganzzahltypen<br />

• logischer Typ (bool)<br />

• Zeichentyp (char), als Spezialfall eines ganzzahligen Werts


Kapitel 4 • Die Typen von C# 53<br />

• Gleitkommatypen<br />

• den Typ decimal<br />

Ganzzahltypen<br />

C# stellt insgesamt neun Ganzzahltypen zur Verfügung: sbyte, byte, short,<br />

ushort, int, uint, long, ulong und den in einem eigenen Abschnitt beschriebenen<br />

Typ char. Die Charakteristika und Wertebereiche dieser Typen sind:<br />

• sbyte repräsentiert vorzeichenbehaftete Integer mit 8 Bit und einem Wertebereich<br />

von -128 bis 127.<br />

• byte repräsentiert vorzeichenlose Integer mit 8 Bit und einem Wertebereich<br />

von 0 bis 255.<br />

• short steht für vorzeichenbehaftete Integer mit 16 Bit und einem Wertebereich<br />

von -32.768 bis 32.767.<br />

• ushort ist das vorzeichenlose Gegenstück zu short und hat einen Wertebereich<br />

von 0 bis 65.535.<br />

• int repräsentiert vorzeichenbehaftete Integer mit 32 Bit und einem Wertebereich<br />

von -2.147.483.648 bis 2.147.483.647.<br />

• uint ist die vorzeichenlose Variante von int mit einem Wertebereich von 0 bis<br />

4.294.967.295.<br />

• long steht für vorzeichenbehaftete Integer mit 64 Bit und einem Wertebereich<br />

von -9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807.<br />

• ulong bezeichnet vorzeichenlose Integer mit 64 Bit und einem Wertebereich<br />

von 0 bis 18.446.744.073.709.551.615.<br />

C- und VB-ProgrammiererInnen werden sich vermutlich über die Wertebereiche<br />

von int und long wundern: Im Gegensatz zu anderen Programmiersprachen ist<br />

C# nicht von der Größe eines Maschinenwortes abhängig, weshalb long hier<br />

unabhängig von der Zielplattform 64 Bit hat.<br />

Logischer Typbool<br />

Der Datentyp bool repräsentiert die Booleschen Werte True und False. Variablen<br />

dieses Typs können Sie sowohl die Literale True und False als auch Boolesche<br />

Ausdrücke als Wert zuweisen:<br />

bool bTest = (80 > 90);<br />

Im Gegensatz zu C und C++ betrachtet C# nicht einfach jeden Integerwert<br />

ungleich 0 als True – und eine implizite Konvertierung zwischen bool und anderen<br />

Integertypen, die diese von den beiden anderen Programmiersprachen her<br />

gewohnte Konvention hinterrücks wieder einführen würde, gibt es nicht.


54<br />

Wertbehaftete Typen<br />

char<br />

Der Datentyp char repräsentiert ein einzelnes Unicode-Zeichen. Solche Zeichen<br />

haben eine Breite von 16 Bit, was zur Darstellung praktisch jeden Schriftzeichens<br />

jeder auf dieser Welt existierenden Sprache ausreichend ist. Die Zuweisung eines<br />

Wertes an eine Variable vom Typ char kann so aussehen:<br />

char chSomeChar = 'A';<br />

Alternativ ist die Definition von Zeichenkonstanten über die von C her bekannten<br />

Escape-Sequenzen möglich, wobei sich wahlweise das Präfix \x für die hexadezimale<br />

Notation und das Präfix \u für die Unicode-Notation einsetzen lassen:<br />

char chSomeChar = '\x65'; // 'e'<br />

char chSomeChar = '\u0065'; // 'e'<br />

Beim Präfix \x nimmt der Compiler gegebenenfalls selbst die Erweiterung auf 16<br />

Bit vor, weshalb \x65, \x065 und \x0065 identische Werte darstellen. Das Präfix \<br />

u erfordert ebenfalls eine hexadezimale Notation, nur sind dort in jedem Fall vier<br />

Stellen gefordert: \u65 bemängelt der Compiler demnach als fehlerhaft.<br />

C# definiert keine impliziten Konvertierungen zwischen char und den anderen<br />

Integertypen. char lässt sich hier also nicht einfach als weiterer Integertyp behandeln<br />

(und stellt deshalb einen weiteren Bereich in C# dar, in dem C-Programmierer<br />

von ihren liebgewordenen Gewohnheiten abgehen müssen). Explizite Typumwandlungen<br />

sind dagegen sehr wohl möglich:<br />

char chSomeChar = (char)65;<br />

int nSomeInt = (int)'A';<br />

Die von C her bekannten Escape-Sequenzen für Zeichenkonstanten übernimmt<br />

C# komplett. Wenn Sie Ihr Gedächtnis in dieser Hinsicht ein wenig auffrischen<br />

wollen oder müssen, hilft Tabelle 4.1 weiter.<br />

Escape-Sequenz Zeichenkonstante<br />

\'<br />

Einfaches Hochkomma<br />

\” Anführungszeichen<br />

\\ Umgekehrter Schrägstrich<br />

\0 Null (Unicode-Wert 0)<br />

\a Alert (Piepton auf dem Terminal)<br />

\b Rückschritt (Backspace)<br />

\f Seitenvorschub (Formfeed)<br />

\n Zeilenvorschub (Newline)<br />

Tab. 4.1: Escape-Sequenzen für Zeichenkonstanten


Kapitel 4 • Die Typen von C# 55<br />

Escape-Sequenz<br />

\r Wagenrücklauf (Carriage Return)<br />

\t Horizontaler Tabulator<br />

\v Vertikaler Tabulator<br />

Tab. 4.1:<br />

Zeichenkonstante<br />

Escape-Sequenzen für Zeichenkonstanten (Forts.)<br />

Gleitkommatypen<br />

Zur Darstellung von Gleitkommawerten definiert C# zwei Typen namens float<br />

und double, die sich sowohl in ihrer Genauigkeit als auch hinsichtlich ihrer Wertebereiche<br />

voneinander unterscheiden:<br />

• float hat einen Wertebereich von 1.5·10 -45 bis 3.4·10 38 mit einer Genauigkeit<br />

von sieben Ziffern<br />

• double hat einen Wertebereich von 5.0·10 -324 bis 1.7·10 308 mit einer Genauigkeit<br />

von 15 bis 16 Ziffern<br />

Mögliche Ergebnisse von Berechnungen mit diesen Datentypen sind:<br />

• positive und negative 0<br />

• positive und negative Unendlichkeit<br />

• numerisch nicht darstellbarer Wert (NaN)<br />

• die endliche Menge der regulären Gleitkommawerte in der gegebenen Genauigkeit<br />

Falls ein numerischer Ausdruck auch nur einen Gleitkommawert erhält, setzt der<br />

Compiler vor der Berechnung alle anderen Werte des Ausdrucks implizit in Gleitkommawerte<br />

um.<br />

decimal<br />

Der Datentyp decimal ist für Finanzberechnungen und andere Operationen vorgesehen,<br />

bei denen es auf maximale Präzision ankommt. Variablen dieses Typs<br />

haben eine Breite von 128 Bit, einen Wertebereich von rund 1.0·10 -28 bis 7.9·10 28<br />

und eine Genauigkeit von 28 bis 29 Ziffern. Bitte beachten Sie, dass Ziffern nicht<br />

in jedem Fall mit Dezimalstellen gleichzusetzen sind: 28 Dezimalstellen Genauigkeit<br />

stellen hier die absolute Obergrenze dar.<br />

Absolut gesehen ist der Wertebereich von decimal offensichtlich erheblich kleiner<br />

als der von double, bei der Präzision ist das Gegenteil der Fall. Aus diesem Grund<br />

nimmt C# keine implizite Typumwandlung zwischen den beiden Typen vor: In<br />

der einen Richtung könnte sonst ein Überlauf entstehen, in der anderen ein<br />

Genauigkeitsverlust. Explizite Typumwandlungen sind dagegen möglich.


56<br />

Wertbehaftete Typen<br />

Da der Compiler Gleitkomma-Konstanten normalerweise als double behandelt,<br />

sollten Sie für Initialisierungen von Variablen des Typs decimal das Suffix m verwenden:<br />

decimal decMyValue = 1.0m;<br />

Dieses Suffix weist den Compiler an, das Literal von vornherein als decimal zu<br />

interpretieren.<br />

4.1.2 Strukturierte Typen<br />

Ein strukturierter Typ kann in C# Konstruktoren, Konstanten, Felder, Methoden,<br />

Eigenschaften, Indizierer, Operatoren und verschachtelte Typen deklarieren.<br />

Auch wenn sich das zunächst nach einer kompletten Klassendefinition anhört: In<br />

C# ist struct ein wertbehafteter Typ, class dagegen ein Referenztyp. (Achtung:<br />

In C++ lassen sich auch Klassen mit dem Schlüsselwort struct definieren,<br />

obwohl das in der Praxis selten geschieht.)<br />

Was bei C# als Grundgedanke hinter struct steht, sind leichtgewichtige Objekte<br />

wie Point, FileInfo und so weiter, die man des Öfteren in größerer Zahl braucht.<br />

Die Definition über struct spart Speicher, weil hier im Gegensatz zu class-<br />

Objekten keine weiteren Referenzen erzeugt werden müssen, und das kann in<br />

einem Array mit einigen tausend Objekten sehr wohl einen gehörigen Unterschied<br />

ausmachen.<br />

Das folgende Listing zeigt die Definition eines einfachen strukturierten Typs, der<br />

IP-Adressen mit vier Feldern des Typs byte repräsentiert. Einen Konstruktor habe<br />

ich hier genauso wie Methoden außen vor gelassen, weil dafür exakt dieselben<br />

Regeln wie für Klassen gelten – mehr dazu im nächsten Kapitel.<br />

Listing 4.1:<br />

Definition eines einfachen strukturierten Typs<br />

1: using System;<br />

2:<br />

3: struct IP<br />

4: {<br />

5: public byte b1,b2,b3,b4;<br />

6: }<br />

7:<br />

8: class Test<br />

9: {<br />

10: public static void Main()<br />

11: {<br />

12: IP myIP;<br />

13: myIP.b1 = 192;<br />

14: myIP.b2 = 168;<br />

15: myIP.b3 = 1;<br />

16: myIP.b4 = 101;


Kapitel 4 • Die Typen von C# 57<br />

17: Console.Write("{0}.{1}.",myIP.b1,myIP.b2);<br />

18: Console.Write("{0}.{1}",myIP.b3,myIP.b4);<br />

19: }<br />

20: }<br />

4.1.3 Aufzählungstypen<br />

Wenn Sie einen eigenen Datentyp deklarieren wollen, dessen Wertebereich aus<br />

einer festen Menge benannter Konstanten bestehen soll, dann ist enum das Mittel<br />

der Wahl. In ihrer einfachsten Form sieht eine solche Deklaration so aus:<br />

enum MonthNames { January, February, March, April };<br />

Da diese Deklaration die Standardvorgaben benutzt, basieren MonthNames-Werte<br />

auf dem Typ int, wobei das Element January den Wert 0 hat. Die folgenden<br />

Bezeichner assoziiert der Compiler mit einem jeweils um eins höheren Wert<br />

(February = 1, March = 2, April = 3). Um dem ersten Element der Aufzählung<br />

explizit einen bestimmten Wert zuzuweisen, schreiben Sie:<br />

enum MonthNames { January = 1, February, March, April };<br />

Der Compiler setzt dann February mit 2 gleich, March mit 3 und so weiter. Die<br />

explizite Wahl bestimmter Werte ist in einer solchen Deklaration für jede der<br />

Konstanten möglich – sogar dann, wenn ein einzelner Wert öfter als einmal<br />

erscheint:<br />

enum MonthDays { January = 31, February = 28, March = 31, April = 30 };<br />

Bei Bedarf können Sie außerdem noch einen anderen Datentyp als int festlegen:<br />

enum MonthDays : byte { January = 31, February = 28, March = 31, April =<br />

30 };<br />

Neben int und byte sind hier auch short und long verwendbar.<br />

4.2 Referenztypen<br />

Im Gegensatz zum wertbehafteten Typ speichert ein Referenztyp die von ihm<br />

repräsentierten Daten nicht direkt, sondern in Form eines Verweises auf einen<br />

Speicherbereich, der die Daten enthält. C# definiert die folgenden Referenztypen:<br />

• object<br />

• class<br />

• Schnittstellen<br />

• Delegationen


58<br />

Referenztypen<br />

• string<br />

• Arrays<br />

4.2.1 object<br />

Der Typ object stellt die grundlegende Basisklasse und damit die Mutter aller<br />

anderen Klassentypen dar. Und weil dem so ist, können Sie einer Variablen des<br />

Typs object Werte jedes anderen Typs zuweisen – auch einen numerischen Wert:<br />

object theObj = 123;<br />

In diesem Zusammenhang eine Warnung an alle C++-ProgrammiererInnen:<br />

object hat nichts, aber auch gar nichts mit void-Zeigern zu tun, nach denen Sie<br />

wahrscheinlich bereits gesucht haben. Zeiger im klassischen Sinne gibt es bei C#<br />

definitiv nicht.<br />

Der Typ object wird unter anderem beim boxing eingesetzt – dem Verkapseln<br />

eines Wertes als Objekt. Eine Beschreibung dieses Konzepts finden Sie gegen<br />

Ende dieses Kapitels.<br />

4.2.2 class<br />

Mit class deklarierte Typen können Mitgliedsdaten, Mitgliedsfunktionen und<br />

eingeschachtelte Typen enthalten. Der Terminus Mitgliedsdaten (»data member«<br />

dient bei C# als Oberbegriff für Datenfelder, Konstanten und Ereignisse; Mitgliedsfunktionen<br />

(»member functions«) sind Methoden, Eigenschaften, Indizierer,<br />

Operatoren, Konstruktoren und Destruktoren. Mit class und struct deklarierte<br />

Typen sind in puncto Funktionalität weitgehend gleich; allerdings geht es,<br />

wie im vorangehenden Abschnitt erläutert, bei struct um Werte, bei class dagegen<br />

um Referenzen.<br />

C# beschränkt sich im Gegensatz zu C++ auf einfache Vererbung: Die Deklaration<br />

einer von mehreren Basisklassen abgeleiteten Klasse ist hier also nicht möglich.<br />

Was C# dagegen ohne Weiteres erlaubt, ist die Deklaration einer Klasse als<br />

gemeinsame Ableitung mehrerer Interfaces (siehe nächster Abschnitt).<br />

Da Klassen und ihrem Einsatz bei der Programmierung das Kapitel 5, »Klassen«,<br />

gewidmet ist, beschränkt sich dieser Abschnitt auf einen allgemeinen Überblick:<br />

Er will lediglich zeigen, wie sich die Klassen von C# in das Typenschema der<br />

Sprache einpassen.<br />

4.2.3 Schnittstellen<br />

Eine Schnittstelle (Interface) deklariert einen Referenztyp, der ausschließlich aus<br />

abstrakten Mitgliedern aufgebaut ist. Ähnliche Konzepte in C++ sind die Mitglieder<br />

eines struct-Typs und auf 0 gesetzten Methode. Falls Ihnen das nichts sagt:


Kapitel 4 • Die Typen von C# 59<br />

In C# besitzt eine Schnittstelle lediglich eine Signatur, aber keinerlei Implementation<br />

oder irgendeinen Codeanteil. Woraus unter anderem folgt, dass man in C#<br />

von einer Schnittstelle keine Instanzen anlegen kann, sondern ausschließlich<br />

Instanzen von Objekten, die von dieser Schnittstelle abgeleitet sind.<br />

Da Sie eine Schnittstelle mit Methoden, Indizierern und Eigenschaften ausstatten<br />

können, stellt sich die Frage, wo dann der Unterschied zu einer Klasse liegt? Eine<br />

Schnittstelle ist eine reine Deklaration, eine Klasse dagegen eine Definition. Der<br />

Unterschied kommt hauptsächlich bei Ableitungen zum Tragen: Wie zuvor<br />

erwähnt, lässt C# bei Ableitungen nur eine Klasse als Basis zu, Schnittstellen<br />

können es dagegen beliebig viele sein.<br />

Nachdem man bei der Ableitung von Schnittstellen offensichtlich die gesamte<br />

Implementation selbst beisteuern muss, wäre noch zu klären, welche Vorteile dieser<br />

Ansatz haben soll. Das geht am einfachsten mit Hilfe eines Beispiels aus dem<br />

NGWS-Programmgerüst: Viele der dort verwendeten Klassen implementieren<br />

beispielsweise die Schnittstelle IDictionary, an die man dann über eine einfache<br />

explizite Typumwandlung herankommt:<br />

IDictionary myDict = (IDictionary)EinObjektMitIDictionaryImplementation;<br />

Nun können Sie mit der IDictionary-Implementation des Objekts arbeiten. Die<br />

Crux daran: Obwohl IDictionary von vielen Klassen (und sicher nicht immer auf<br />

die gleiche Weise) implementiert wird, ist die Schnittstelle und damit der<br />

Umgang mit den entsprechenden Objekten grundsätzlich gleich. Wenn Sie also<br />

den Zugriff auf IDictionary einmal implementiert haben, können Sie den Code<br />

jederzeit auch an anderer Stelle wiederverwenden.<br />

Wer eigene Schnittstellen in seinen Klassen einsetzen will, sollte natürlich einiges<br />

über objektorientiertes Design wissen – und damit über ein Thema, das den Rahmen<br />

dieses Buches definitiv sprengt. Wie die Syntax zur Definition von Interfaces<br />

aussieht, ist dagegen schnell erklärt. Der folgende Codeauszug definiert eine<br />

Schnittstelle namens IFace mit nur einer Methode:<br />

interface IFace<br />

{<br />

void ShowMyFace();<br />

}<br />

Wie bereits erwähnt, können Sie mit dieser Definition allein noch kein Objekt<br />

anlegen – sie dient ausschließlich als Basis zur Ableitung einer Klasse. Die<br />

Klasse muss ihrerseits die (von der Schnittstelle definierte abstrakte) Methode<br />

ShowMyFace implementieren:


60<br />

Referenztypen<br />

class CFace:IFace<br />

{<br />

public void ShowMyFace()<br />

{<br />

Console.WriteLine("implementation");<br />

}<br />

}<br />

Und noch einmal: Der einzige Unterschied zwischen Klassen und Schnittstellen<br />

besteht darin, dass Schnittstellen für ihre Mitglieder keine Implementation bereitstellen.<br />

Alle weiteren Details finden Sie in Kapitel 5, »Klassen«.<br />

4.2.4 Delegationen<br />

4.2.5 string<br />

Eine Delegation (Delegate – direkt übersetzt: »Abgesandter«) verkapselt eine<br />

Methode mit einer bestimmten Signatur. Im Wesentlichen handelt es sich dabei<br />

um die typensichere und überwachte Variante der aus C und C++ bekannten<br />

Funktionszeiger. Die Instanz einer Delegation kann sowohl eine statische als auch<br />

eine Instanz-gebundene Methode verkapseln.<br />

Obwohl man Delegationen ganz normal für Methoden verwenden kann, liegt der<br />

Schwerpunkt ihres Einsatzes in C#-Programmen auf Ereignissen – also auf Rückruffunktionen<br />

von Klassen. Details dazu finden Sie gleichfalls in Kapitel 5,<br />

»Klassen«.<br />

Auch wenn das C-Programmierer vielleicht überrascht: Der Typ string zur Speicherung<br />

und Manipulation von Zeichenfolgen gehört bei C# zu den elementaren<br />

Datentypen. Er ist eine direkte Ableitung von object, die als »versiegelt« gekennzeichnet<br />

ist – d.h. nicht als Basisklasse für eigene Ableitungen eingesetzt werden<br />

kann. Wie alle anderen Typen von C# stellt auch string einen Alias für eine vordefinierte<br />

Klasse dar, nämlich:<br />

System.String;<br />

Der Einsatz gestaltet sich ausgesprochen einfach:<br />

string myString = "irgendein Text";<br />

Das Aneinanderhängen zweier Zeichenfolgen ist auch nicht wesentlich komplizierter:<br />

string myString = "irgendein Text" + " und noch etwas";<br />

Für den Zugriff auf einzelne Zeichen einer Zeichenfolge lässt sich der Indizierer<br />

dieser Klasse verwenden:<br />

char chFirst = myString[0];


Kapitel 4 • Die Typen von C# 61<br />

4.2.6 Arrays<br />

Und der Vergleich zweier Zeichenfolgen geschieht mit dem von dieser Klasse als<br />

eigene Variante bereitgestellten Operator ==:<br />

if (myString == yourString) ...<br />

Es sei angemerkt, dass sich dieser Operator auf die Werte, also die Zeichenfolgen,<br />

bezieht, obwohl string als Klasse implementiert ist und somit einen Referenztyp<br />

darstellt. (Der Standardoperator == von C# würde bei einem Referenztyp prüfen,<br />

ob die Speicheradressen, auf die myString und yourString verweisen, identisch<br />

sind.)<br />

Details zu den einzelnen Methoden der Klasse string finden Sie in den Beispielen<br />

der folgenden Kapitel. (Tatsächlich kommen nur wenige Beispiele dieses<br />

Buchs ohne string und die dazugehörigen Methoden aus.)<br />

Arrays stellen eine Sammlung von gleichartigen Variablen dar, die hier als Elemente<br />

bezeichnet werden, sämtlich den gleichen Typ haben müssen und über<br />

einen Index angesprochen werden. Was den Typ der Elemente (auch als »Typ des<br />

Arrays« bezeichnet) betrifft, gibt es keine Beschränkungen: Ein Array kann Integer-Werte,<br />

Zeichenfolgen oder Objekte beliebigen Typs speichern.<br />

Die Zahl der Dimensionen eines Arrays – also die Zahl der Indizes, die für den<br />

Zugriff auf ein einzelnes Element angegeben werden müssen –, wird bei C# als<br />

Rang bezeichnet. Mit Abstand am häufigsten kommen eindimensionale Arrays<br />

zum Einsatz, die folglich den Rang eins haben; mehrdimensionale Arrays haben<br />

dagegen einen Rang größer eins. Die Zählung der Indizes beginnt mit 0 und läuft<br />

bis zur der Größe der jeweiligen Dimension minus eins.<br />

Damit erst einmal genug der grauen Theorie. Die Deklaration eines eindimensionalen<br />

Arrays mit gleichzeitiger Initialisierung der Elemente kann so aussehen:<br />

string[] arrLanguages = { "C", "C++", "C#" };<br />

Tatsächlich stellt diese Notation die Abkürzung dar für:<br />

arrLanguages[0]="C"; arrLanguages[1]="C++"; arrLanguages[2]="C#";<br />

Mehrdimensionale Arrays lassen sich ebenfalls in einem Schritt deklarieren und<br />

initialisieren:<br />

int[,] arr = {{0,1}, {2,3}, {4,5}};<br />

Der Compiler betrachtet dies als Abkürzung für:<br />

arr[0,0] = 0; arr[0,1] = 1;<br />

arr[1,0] = 2; arr[1,1] = 3;<br />

arr[2,0] = 4; arr[2,1] = 5;


62<br />

Boxing und Unboxing<br />

Wenn die Größe eines Arrays feststeht, im Rahmen der Deklaration aber keine<br />

explizite Initialisierung gewünscht ist, kann auch der new-Operator zum Einsatz<br />

kommen:<br />

int [,] myArr = new int[5,3];<br />

Und schließlich: Wenn sich die Größe der einzelnen Dimensionen erst zur Laufzeit<br />

bestimmen lässt, dann wird new mit Variablen anstelle von Konstanten aufgerufen:<br />

int nVar = 5;<br />

// ...<br />

int[] arrToo = new int[nVar];<br />

Wie am Anfang dieses Abschnitts erwähnt, lassen sich Arrays als Sammelbecken<br />

für Alles und Jedes verwenden – vorausgesetzt, sämtliche Elemente haben denselben<br />

Typ. Um Werte mit beliebigen Typen in ein Array hineinstecken zu können,<br />

müssen Sie lediglich dafür sorgen, dass es den Typ object trägt – für den<br />

Rest sorgt das Boxing von C#.<br />

4.3 Boxing und Unboxing<br />

Nachdem dieses Kapitel eine ganze Reihe von Typen vorgestellt und die Unterschiede<br />

zwischen Variablen- und Referenztypen erläutert hat, sollte klar sein:<br />

Solange es um Geschwindigkeit geht, wird man wertbehaftete Typen gegenüber<br />

Referenztypen bevorzugen, weil sich hinter Werten auch in C# letztlich nichts<br />

weiter als einfache Speicherblocks verbergen. Mitunter kann es aber auch von<br />

Vorteil sein, die von Objekten gebotenen Bequemlichkeiten für einen wertbehafteten<br />

Typ zu nutzen.<br />

Kein Problem. C# definiert einen Mechanismus, der sozusagen das Bindeglied<br />

zwischen wertbehafteten Typen und Referenztypen liefert, indem er die Konvertierung<br />

wertbehafteter Typen in den Typ object und wieder zurück ermöglicht.<br />

Anders gesagt: In C# lässt sich alles und jedes letztlich als Objekt verpacken,<br />

sofern ein solches gefordert ist – und es gibt auch eine Rückfahrkarte.<br />

4.3.1 Umwandeln von Werten in Objekte<br />

Der Terminus Boxing steht bei C# für die implizite Umwandlung eines Werts in<br />

den Typ object und damit bildhaft eben für das Einpacken in einen Karton: Dabei<br />

wird eine neue Instanz des Typs object angelegt, das ein Element des entsprechenden<br />

wertbehafteten Typs besitzt, und diesem Element eine Kopie des Werts<br />

zugewiesen. Ein einfaches Beispiel dazu:<br />

int nFunny = 2000;<br />

object oFunny = nFunny;


Kapitel 4 • Die Typen von C# 63<br />

Die Zuweisung in der zweiten Zeile stellt eine solche implizite Typumwandlung<br />

dar: Sie generiert das Objekt und weist ihm nFunny als Wert zu. Danach liegen<br />

sowohl die Integer- als auch die Objektvariable auf dem Stack, und außerdem<br />

existiert eine Kopie von nFunny auf dem Heap des Programms.<br />

Was impliziert das? Nun, die Werte der beiden Variablen sind unabhängig voneinander<br />

– oFunny enthält keinen Querverweis auf nFunny, sondern einen Querverweis<br />

auf eine Kopie dieses Werts. Der folgende Codeauszug illustriert die Konsequenzen:<br />

int nFunny = 2000;<br />

object oFunny = nFunny;<br />

oFunny = 2001;<br />

Console.WriteLine("{0} {1}", nFunny, oFunny);<br />

Wie nach den vorangehenden Erläuterungen wohl auch nicht anders zu erwarten,<br />

bleibt nFunny von der Wertänderung von oFunny unberührt, und das Programm<br />

gibt »2000 2001« aus. Solange Sie diesen Punkt im Kopf behalten, steht einer<br />

Nutzung der Objektfunktionalität für wertbehaftete Typen nichts weiter im Wege.<br />

4.3.2 Umwandeln von Objekten in Werte<br />

Das Unboxing (Auspacken) eines Wertes ist im Gegensatz zu seiner Verpackung<br />

als Objekt eine Operation, die Sie explizit anfordern müssen – nicht zuletzt, weil<br />

der Compiler von Ihnen wissen will, welchen Datentyp Sie in dem Objekt erwarten.<br />

C# prüft bei einer solchen Anforderung, ob das Objekt auch tatsächlich einen<br />

Wert des gewünschten Datentyps enthält: Wenn ja, wird dieser Wert ausgepackt<br />

und an der Variable als Kopie zugewiesen. Ein Beispiel dazu:<br />

int nFunny = 2000;<br />

object oFunny = nFunny;<br />

int nFunny2 = (int)oFunny;<br />

Auf die Anforderung eines nicht im Objekt enthaltenen Datentyps – beispielsweise<br />

mit<br />

double nNotSoFunny = (double)oFunny;<br />

reagiert das NGWS-Laufzeitsystem mit einer Ausnahme des Typs Invalid-<br />

CastException. (Details dazu finden Sie in Kapitel 7, »Ausnahmen behandeln«.)


64<br />

Zusammenfassung<br />

4.4 Zusammenfassung<br />

Dieses Kapitel hat die von C# vorgegebenen Typen vorgestellt. Zu den einfachen<br />

Typen gehören die Integertypen, bool, char, die Gleitkommatypen und decimal,<br />

mit denen man die meisten mathematischen und finanzmathematischen Berechnungen<br />

und logische Bedingungen formulieren wird.<br />

Vor der Einführung der Referenztypen ging es kurz um struct, einen engen Verwandten<br />

von class. Typen dieser Art verhalten sich im Prinzip wie Klassen, zählen<br />

aber zu den wertbehafteten Typen. Ihr erheblich geringerer Überbau macht sie<br />

vor allem dann zum Mittel der Wahl, wenn es um größere Mengen kleinerer<br />

Objekte geht.<br />

Im Abschnitt über Referenztypen ging es zuerst um object, die Mutter aller Klassen<br />

in C#, die außerdem beim Boxing und Unboxing von Werten zum Einsatz<br />

kommt. Des Weiteren wurden Delegationen, Strings und Arrays besprochen.<br />

Die Art von Datentyp, die angehende C#-ProgrammiererInnen wohl auch noch in<br />

ihren Träumen verfolgen wird, ist die Klasse. Sie bildet sozusagen der Kern der<br />

objektorientierten Programmierung mit dieser Sprache. Das nächste Kapitel geht<br />

denn auch in Länge und Breite auf dieses Konzept mit all seinen Möglichkeiten<br />

ein.


Kapitel 5<br />

Klassen<br />

5.1 Konstruktoren und Destruktoren 66<br />

5.2 Methoden 68<br />

5.3 Eigenschaften 77<br />

5.4 Indizierer 78<br />

5.5 Ereignisse 80<br />

5.6 Modifizierer 83<br />

5.7 Zusammenfassung 87


66<br />

Konstruktoren und Destruktoren<br />

Nachdem sich das vorangehende Kapitel ausführlich mit den Datentypen von C#<br />

und ihrem Einsatz beschäftigt hat, geht es nun um das wichtigste Konstrukt dieser<br />

Sprache: Die Klasse. Ohne eine Klasse ließe sich ein C#-Programm nicht einmal<br />

kompilieren. Das folgende Material geht davon aus, dass Ihnen die grundlegenden<br />

Bausteine von Klassen geläufig sind: Methoden, Eigenschaften, Konstruktoren<br />

und Destruktoren. C# bereichert diese Palette sowohl um Indizierer als auch um<br />

Ereignisse.<br />

Im Einzelnen geht es in diesem Kapitel um:<br />

• den praktischen Einsatz von Konstruktoren und Destruktoren<br />

• das Schreiben von Methoden für Klassen<br />

• Zugriffsfunktionen für Eigenschaften<br />

• die Implementation von Indizierern<br />

• die Definition von Ereignissen und die Benachrichtigung von Clients über Delegationen<br />

• Modifizierer für Klassen, ihre Methoden und Zugriffsfunktionen<br />

5.1 Konstruktoren und Destruktoren<br />

Als Erstes kommen bei einer Klasse beziehungsweise ihren Objekten die Anweisungen<br />

aus dem Konstruktor der Klasse zur Ausführung – und zwar noch bevor<br />

Sie die erste Methode oder Eigenschaft des Objekts aufrufen können. Das gilt<br />

auch, wenn Sie für eine Klasse überhaupt keinen eigenen Konstruktor definieren,<br />

weil der Compiler dann einen Standardkonstruktor einsetzt:<br />

class TestClass<br />

{<br />

public TestClass(): base() {} // vom Compiler eingesetzt<br />

}<br />

Konstruktoren haben grundsätzlich denselben Namen wie ihre Klasse und liefern<br />

keinen Funktionswert zurück. Im Normalfall sind sie öffentlich deklariert und<br />

werden zur Initialisierung von Elementvariablen verwendet:<br />

public TestClass()<br />

{<br />

// Initialisierungscode für<br />

// Elementvariablen usw.<br />

}


Kapitel 5 • Klassen 67<br />

Für Klassen, die ausschließlich statische Elemente definieren (also Methoden,<br />

Eigenschaften, Elementvariablen usw., die für den Typ an sich definiert sind –<br />

nicht für einzelne Objekte des jeweiligen Typs), kann man einen private-Konstruktor<br />

definieren:<br />

private TestClass() {}<br />

Auch wenn das einen Vorgriff auf den Abschnitt dieses Kapitels darstellt, in dem<br />

es um Modifizierer geht: private teilt dem Compiler mit, dass dieser Konstruktor<br />

ausschließlich innerhalb der Klasse zur Verfügung steht und vom restlichen Programm<br />

aus nicht aufgerufen werden kann. Deshalb lassen sich auch keine<br />

Objekte dieser Klasse anlegen.<br />

Bei Konstruktoren sind Sie nicht auf die parameterlose Variante beschränkt. Sie<br />

können ohne weiteres Konstruktoren definieren, die eine beliebige Zahl von<br />

Argumenten zur Initialisierung von Datenfeldern usw. erwarten:<br />

public TestClass(string strName, int nAge) { ... }<br />

In C und C++ stattet man Klassen recht oft mit einer zusätzlichen Initialisierungsmethode<br />

aus, weil Konstruktoren dort ebenfalls keine direkten Ergebnisse liefern<br />

(über die sich signalisieren ließe, ob bestimmte Voraussetzungen – wie etwa das<br />

Vorhandensein anderer Objekte – für die Konstruktion erfüllt sind). In C# sind<br />

Konstruktoren zwar ebenfalls »ergebnislos«, separate Initialisierungsmethoden<br />

aber trotzdem selten notwendig, weil man hier im Falle eines Falles innerhalb von<br />

Konstruktoren eine selbstdefinierte Ausnahme auslösen kann. (Details dazu finden<br />

Sie in Kapitel 7, »Ausnahmen behandeln«.)<br />

Der Destruktor einer Klasse stellt das Gegenteil ihres Konstruktors dar, ist also<br />

für die Deinitialisierung zuständig. Der Name dieser Routine ist ebenfalls vorgegeben:<br />

Er besteht aus dem Klassennamen mit einer vorangestellten Tilde:<br />

public ~TestClass()<br />

{<br />

// Aufräumen<br />

}<br />

Hier kann sich sehr wohl die Notwendigkeit zum Schreiben einer separaten Aufräumfunktion<br />

ergeben – nämlich dann, wenn Objekte der Klasse vergleichsweise<br />

kostbare Ressourcen (wie größere Bereiche auf dem Heap) belegen. Wozu dieses<br />

Extra, wenn die C#-Laufzeitumgebung doch einen Garbage Collector enthält, der<br />

automatisch den Destruktor von Objekten aufruft, die den Geltungsbereich verlassen<br />

haben? Weil diese Automatik nur in bestimmten Zeitabständen in Gang<br />

gesetzt wird und außerdem noch von anderen Umständen wie der aktuellen<br />

Speicherbelegung abhängt – mithin durchaus die Möglichkeit besteht, dass ein<br />

Objekt Ressourcen wesentlich länger belegt als vom Programmierer beabsichtigt.


68<br />

Methoden<br />

Für das Aufräumen zu exakt definierten Zeitpunkten empfiehlt es sich deshalb,<br />

eine separate Methode zu schreiben, die dann ihrerseits auch vom Destruktor aufgerufen<br />

wird:<br />

public void Release()<br />

{<br />

// alle vom Objekt belegten Ressourcen freigeben<br />

}<br />

public ~TestClass()<br />

{<br />

Release();<br />

// weitere Aufräumarbeiten, falls notwendig<br />

}<br />

Rein technisch gesehen ist der Aufruf von Release im Destruktor nicht unbedingt<br />

notwendig, weil sich die Garbage Collection von sich aus um die Freigabe des<br />

Objekts kümmert. Nur zu welchem Zeitpunkt, ist die Frage – und außerdem ist es<br />

bewährte Programmierpraxis, das Aufräumen nicht zu vergessen.<br />

5.2 Methoden<br />

Mit einem Konstruktor und einem Destruktor ausgerüstete Objekte lassen sich<br />

wunderbar anlegen und wieder freigeben, bieten aber außer Speicherplatzverbrauch<br />

definitiv keine Funktionalität. In den meisten Fällen wird man den größten<br />

Teil der Funktionalität in Form von Methoden implementieren. Einige Beispiele<br />

für statische Methoden haben Sie in diesem Buch bereits zu sehen bekommen.<br />

Solche Methoden sind allerdings Ausstattung der Klasse (also des Datentyps an<br />

sich) und nicht der einzelnen Instanz (Objekt).<br />

Damit die unvermeidlichen Fragen zu Methoden so schnell und übersichtlich wie<br />

möglich beantwortet werden, folgen hier drei Abschnitte:<br />

• Parameter von Methoden<br />

• Überschreiben von Methoden<br />

• Verbergen von Methoden<br />

5.2.1 Parameter von Methoden<br />

Eine Methode, die wechselnde Werte bearbeiten können soll, muss einerseits eine<br />

Möglichkeit zur Übergabe von Werten definieren, andererseits ein Ergebnis (in<br />

irgend einer Form) zurückliefern. Die folgenden drei Abschnitte beschäftigen<br />

sich mit den diversen Formen der Datenübergabe, die C# im Zusammenhang mit<br />

Methoden zu bieten hat:


Kapitel 5 • Klassen 69<br />

• Eingangsparameter<br />

• Referenzparameter<br />

• Ausgangsparameter<br />

Eingangsparameter<br />

Diese Art von Parametern haben Sie bereits in den Beispielen der vorangehenden<br />

Kapitel gesehen. Hier geht es um die schlichte Wertübergabe. Der Compiler versorgt<br />

die Methode mit einer Kopie des übergebenen Werts. Das folgende Listing<br />

zeigt diese Technik:<br />

Listing 5.1:<br />

Wertübergabe von Parametern<br />

1: using System;<br />

2:<br />

3: public class SquareSample<br />

4: {<br />

5: public int CalcSquare(int nSideLength)<br />

6: {<br />

7: return nSideLength*nSideLength;<br />

8: }<br />

9: }<br />

10:<br />

11: class SquareApp<br />

12: {<br />

13: public static void Main()<br />

14: {<br />

15: SquareSample sq = new SquareSample();<br />

16: Console.WriteLine(sq.CalcSquare(25).ToString());<br />

17: }<br />

18: }<br />

Da CalcSquare einen echten Wert (exakter: die Kopie eines Werts) und keine<br />

Referenz auf eine Variable erwartet, ist zum Aufruf dieser Methode in Zeile 16<br />

eine Konstante verwendbar. CalcSquare hat ein direktes Funktionsergebnis (Typ<br />

int), das erst in eine Zeichenfolge umgesetzt und dann an Console.WriteLine<br />

übergeben wird. Die explizite Deklaration einer temporären Variablen zum Zwischenspeichern<br />

des Ergebnisses von CalcSquare werden Sie in diesem Listing<br />

nicht finden, weil sie unnötig ist bzw. sich der Compiler um dieses Detail gegebenenfalls<br />

schon selbst kümmert.<br />

Für C/C++-Programmierer stellen Eingangsparameter und die Wertübergabe<br />

absolut nichts Neues dar. Wenn Sie Ihre Erfahrungen mit Visual Basic gesammelt<br />

haben, müssen Sie vorsichtig sein: Solange Sie nicht explizit einen Modifizierer<br />

angeben, findet bei C# standardmäßig eine Wertübergabe statt – der C#-Compiler<br />

baut hier also ein implizites ByVal ein, und nicht ein ByRef.


70<br />

Methoden<br />

Moment mal. Was geschieht bei der Übergabe eines string-Wertes oder eines<br />

beliebigen anderen Objekts? Geht es da nicht per se um Referenzen? Ganz recht.<br />

Hier sollte ich also meine zuvor gemachte Aussage relativieren: Für manche<br />

Datentypen heißt Wertübergabe: Übergabe einer Kopie der Referenz auf den<br />

Wert. Um etwas weiter auszuholen: Bei COM ist alles und jedes eine Schnittstelle,<br />

und jede Klasse kann eine oder mehrere Schnittstellen haben. Eine Schnittstelle<br />

ist nicht mehr und nicht weniger als eine Sammlung von Funktionszeigern;<br />

Daten enthält sie nicht. Das Kopieren einer solchen Zeigersammlung wäre<br />

schlichte Platzverschwendung, weshalb eine Methode bei der Wertübergabe<br />

lediglich die Startadresse dieser Sammlung zu sehen bekommt – also die Information,<br />

die auch der Aufrufer hat.<br />

Referenzparameter<br />

Mit Eingangsparametern und direkten Funktionsergebnissen lässt sich zwar<br />

bereits eine Menge anfangen, wenn es allerdings darum geht, den Wert einer Variable<br />

(also den Speicherbereich, der mit dieser Variable assoziiert ist) des Aufrufers<br />

zu verändern, sind die Grenzen erreicht. Für solche Aufgaben sind Referenzparameter<br />

(kurz: ref-Parameter) zuständig:<br />

void MyMethod(ref int nInOut)<br />

Da diese Methode eine echte Variable übergeben bekommt, deren Wert sie jederzeit<br />

lesen (und ändern) kann, muss der Aufrufer für die Initialisierung der Variablen<br />

sorgen. Ansonsten beschwert sich der Compiler. Das folgende Listing zeigt<br />

eine Methode, die mit einem ref-Parameter arbeitet.<br />

Listing 5.2:<br />

Übergabe von Parametern als Referenz<br />

1: // class SquareSample<br />

2: using System;<br />

3:<br />

4: public class SquareSample<br />

5: {<br />

6: public void CalcSquare(ref int nOne4All)<br />

7: {<br />

8: nOne4All *= nOne4All;<br />

9: }<br />

10: }<br />

11:<br />

12: class SquareApp<br />

13: {<br />

14: public static void Main()<br />

15: {<br />

16: SquareSample sq = new SquareSample();<br />

17:<br />

18: int nSquaredRef = 20; // muss initialisiert werden<br />

19: sq.CalcSquare(ref nSquaredRef);


Kapitel 5 • Klassen 71<br />

20: Console.WriteLine(nSquaredRef.ToString());<br />

21: }<br />

22: }<br />

Wie in diesem Listing zu sehen, beschränken sich die Änderungen im Wesentlichen<br />

auf den Modifizierer ref, der sowohl bei der Definition als auch beim Aufruf<br />

anzugeben ist. Da die Variable nSquaredRef per Referenz übergeben wird, kann<br />

sie die Methode sowohl als Grundlage der Berechnung als auch zum Zurückliefern<br />

des Ergebnisses verwenden. In ernsthaften Anwendungen empfiehlt es sich<br />

allerdings immer, bei solchen Berechnungen voneinander getrennte Eingangsund<br />

Ausgangsparameter zu verwenden.<br />

Ausgangsparameter<br />

Wie der Name bereits impliziert, sind mit out gekennzeichnete Parameter ausschließlich<br />

für die Rückgabe eines Ergebnisses gedacht. Ausgangsparameter<br />

(kurz: out-Parameter) stellen Referenzen auf Variablen des Aufrufers dar, in<br />

denen Methoden ihre Ergebnisse zurückgeben können. Werte an eine Methode<br />

übergeben kann man mit ihnen nicht, weshalb auf der Seite des Aufrufers auch<br />

keine Initialisierung der jeweiligen Variablen notwendig ist. Das folgende Listing<br />

zeigt eine Methode, die mit einem out-Parameter arbeitet.<br />

Listing 5.3:<br />

Einsatz eines Ausgangsparameters<br />

1: using System;<br />

2:<br />

3: public class SquareSample<br />

4: {<br />

5: public void CalcSquare(int nSideLength, out int nSquared)<br />

6: {<br />

7: nSquared = nSideLength * nSideLength;<br />

8: }<br />

9: }<br />

10:<br />

11: class SquareApp<br />

12: {<br />

13: public static void Main()<br />

14: {<br />

15: SquareSample sq = new SquareSample();<br />

16:<br />

17: int nSquared; // keine Initialisierung notwendig<br />

18: sq.CalcSquare(15, out nSquared);<br />

19: Console.WriteLine(nSquared.ToString());<br />

20: }<br />

21: }


72<br />

Methoden<br />

5.2.2 Überschreiben von Methoden<br />

Die Polymorphie stellt eines der wichtigeren Prinzipien objektorientierter Programmierung<br />

dar. Wenn wir den theoretisch-philosophischen Überbau einmal<br />

außen vor lassen, bedeutet das, dass Sie in einer abgeleiteten Klasse Methoden<br />

der Basisklasse erneut definieren (also ersetzen) können – vorausgesetzt, der<br />

Schöpfer der Basisklasse hat die jeweilige Methode als überschreibbar gekennzeichnet.<br />

Diese Kennzeichnung geschieht in C# mit dem Schlüsselwort virtual:<br />

virtual void CanBOverridden()<br />

Wenn Sie diese Methode in einer abgeleiteten Klasse durch eine eigene Variante<br />

ersetzen wollen, stellen Sie der Deklaration das Schlüsselwort override voran:<br />

override void CanBeOverridden()<br />

Die Sichtbarkeit einer Methode lässt sich durch Überschreiben in abgeleiteten<br />

Klassen übrigens nicht ändern. Mehr dazu und zu den entsprechenden Modifizierern<br />

weiter hinten in diesem Kapitel.<br />

Bemerkenswerter noch als die Möglichkeit, von einer Basisklasse vererbte<br />

Methoden in einer abgeleiteten Klasse umdefinieren zu können, ist der Effekt,<br />

dass die neuen Varianten dieser Methoden auch nach einer expliziten Typumwandlung<br />

in die Basisklasse noch greifen:<br />

((BaseClass)DerivedClassInstance).CanBeOverridden();<br />

Hier wird also die Methode CanBeOverriden der abgeleiteten Klasse aufgerufen<br />

– und nicht die Basisvariante. Das folgende Listing demonstriert dieses Prinzip<br />

etwas ausführlicher. Es definiert eine Basisklasse Triangle, deren Methode zur<br />

Flächenberechnung (ComputeArea) in einer abgeleiteten Klasse durch einen neue<br />

Variante ersetzt wird.<br />

Listing 5.4:<br />

Überschreiben einer Methode der Basisklasse<br />

1: using System;<br />

2:<br />

3: class Triangle<br />

4: {<br />

5: public virtual double ComputeArea(int a, int b, int c)<br />

6: {<br />

7: // Heronsche Formel<br />

8: double s = (a + b + c) / 2.0;<br />

9: double dArea = Math.Sqrt(s*(s-a)*(s-b)*(s-c));<br />

10: return dArea;<br />

11: }<br />

12: }<br />

13:<br />

14: class RightAngledTriangle:Triangle


Kapitel 5 • Klassen 73<br />

15: {<br />

16: public override double ComputeArea(int a, int b, int c)<br />

17: {<br />

18: double dArea = a*b/2.0;<br />

19: return dArea;<br />

20: }<br />

21: }<br />

22:<br />

23: class TriangleTestApp<br />

24: {<br />

25: public static void Main()<br />

26: {<br />

27: Triangle tri = new Triangle();<br />

28: Console.WriteLine(tri.ComputeArea(2, 5, 6));<br />

29:<br />

30: RightAngledTriangle rat = new RightAngledTriangle();<br />

31: Console.WriteLine(rat.ComputeArea(3, 4, 5));<br />

32: }<br />

33: }<br />

Die Methode Triangle.ComputeArea erwartet drei Parameter vom Typ int, liefert<br />

ein Ergebnis des Typs double und ist öffentlich verfügbar. RightAngledTriangle,<br />

die Ableitung der Klasse Triangle, ersetzt die Methode ComputeArea durch eine<br />

eigene Variante, die eine wesentlich einfachere Flächenberechung ausführt. Die<br />

Methode Main des Programms legt von jeder der Klassen ein Objekt an und ruft<br />

dann die (klassenspezifische) Variante von ComputeArea auf.<br />

Was vor allem wohl noch fehlt, ist eine Erläuterung zur Zeile 14 dieses Listings:<br />

class RightAngledTriangle : Triangle<br />

Der Doppelpunkt (:) in dieser Anweisung teilt dem Compiler mit, dass Right-<br />

AngledTriangle von Triangle abgeleitet ist – und stellt auch bereits das gesamte<br />

Drumherum dar, das C# bei Ableitungen sehen will.<br />

Wenn Sie sich die Implementation von ComputeArea in der Klasse Right-<br />

AngledTriangle genauer ansehen, werden Sie feststellen, dass der dritte Parameter<br />

dort gar nicht ausgewertet wird. Er ließe sich für die Prüfung benutzen, ob das<br />

Dreieck auch wirklich rechtwinklig ist – und stellt eine gute Gelegenheit dar, Aufrufe<br />

der Basisklassenvariante von Methoden aus abgeleiteten Klassen heraus zu<br />

demonstrieren:<br />

Listing 5.5:<br />

Aufruf der Basisklassenvariante<br />

1: class RightAngledTriangle:Triangle<br />

2: {<br />

3: public override double ComputeArea(int a, int b, int c)<br />

4: {


74<br />

Methoden<br />

5: const double dEpsilon = 0.0001;<br />

6: double dArea = 0;<br />

7: if (Math.Abs((a*a + b*b – c*c)) > dEpsilon)<br />

8: {<br />

9: dArea = base.ComputeArea(a,b,c);<br />

10: }<br />

11: else<br />

12: {<br />

13: dArea = a*b/2.0;<br />

14: }<br />

15:<br />

16: return dArea;<br />

17: }<br />

18: }<br />

Für diese Prüfung wird die Formel von Pythagoras benutzt, die für rechtwinklige<br />

Dreiecke das Ergebnis 0 liefert. Auf ein von Null (und einem hier als Minimalwert<br />

eingeführten Epsilon) abweichendes Ergebnis reagiert die Methode mit dem<br />

Aufruf der Basisklassenvariante:<br />

dArea = base.ComputeArea(a, b, c);<br />

Der springende Punkt ist hier der Qualifizierer base, über den sich zu jedem Zeitpunkt<br />

Elemente der Basisklasse ansprechen lassen. Derartige Rückgriffe wird<br />

man vor allem dann einsetzen, wenn die Funktionalität der Basisklasse gefordert<br />

ist (die man in einer abgeleiteten Klasse natürlich nicht duplizieren möchte).<br />

5.2.3 Verbergen von Methoden<br />

Eine zweiter Weg zum Ersatz von Methoden besteht darin, die Bezeichner der<br />

Basisklasse zu verbergen. Diese Alternative ist unter anderem bei Ableitungen<br />

von Klassen nützlich, die Sie nicht selbst geschrieben haben (und deren Methoden<br />

unter Umständen das Schlüsselwort virtual fehlt). Sehen Sie sich einmal das<br />

folgende Listing an, stellen Sie sich dabei vor, BaseClass sei von jemand anderem<br />

geschrieben worden, und Sie wollten nun DerivedClass von dieser Klasse ableiten:<br />

Listing 5.6:<br />

Die abgeleitete Klasse implementiert eine in der Basisklasse nicht<br />

vorhandene Methode<br />

1: using System;<br />

2:<br />

3: class BaseClass<br />

4: {<br />

5: }<br />

6:<br />

7: class DerivedClass:BaseClass


Kapitel 5 • Klassen 75<br />

8: {<br />

9: public void TestMethod()<br />

10: {<br />

11: Console.WriteLine("DerivedClass::TestMethod");<br />

12: }<br />

13: }<br />

14:<br />

15: class TestApp<br />

16: {<br />

17: public static void Main()<br />

18: {<br />

19: DerivedClass test = new DerivedClass();<br />

20: test.TestMethod();<br />

21: }<br />

22: }<br />

In diesem Beispiel implementiert DerivedClass irgend ein zusätzliches Feature<br />

über die neue Methode TestMethod. So weit, so gut. Was aber, wenn der Entwickler<br />

von BaseClass selbst schon auf die Idee gekommen ist, TestMethod als<br />

Bezeichner für eine Methode zu verwenden, und dieser Methode auch noch dieselbe<br />

Signatur zu geben (also gleiche Parametertypen in gleicher Reihenfolge –<br />

oder eben überhaupt keine Parameter)? Das folgende Listing simuliert diese Konstellation:<br />

Listing 5.7:<br />

Die Basisklasse implementiert eine gleichnamige Methode<br />

1: class BaseClass<br />

2: {<br />

3: public void TestMethod()<br />

4: {<br />

5: Console.WriteLine("BaseClass::TestMethod");<br />

6: }<br />

7: }<br />

8:<br />

9: class DerivedClass:BaseClass<br />

10: {<br />

11: public void TestMethod()<br />

12: {<br />

13: Console.WriteLine("DerivedClass::TestMethod");<br />

14: }<br />

15: }<br />

Tatsächlich hätten Sie in einer klassischen Programmiersprache nun ein hübsches<br />

Problem am Hals. Kein Wunder, dass sich der C#-Compiler recht gesprächig<br />

zeigt:


76<br />

Methoden<br />

hiding2.cs(13,14): warning CS0114: 'DerivedClass.TestMethod()' hides inherited<br />

member 'BaseClass.TestMethod()'. To make the current method<br />

override that implementation, add the override keyword. Otherwise add<br />

the new keyword.<br />

Übersetzt: 'DerivedClass.TestMethod()' verbirgt das vererbte Mitglied 'Base-<br />

Class.TestMethod()'. Wenn Sie die Implementation [der Basisklasse] durch die<br />

aktuelle Methode ersetzen wollen, fügen Sie das Schlüsselwort override hinzu.<br />

Ansonsten verwenden Sie das Schlüsselwort new.<br />

Mit dem Modifizierer new teilen Sie dem Compiler mit, dass die Methode der<br />

abgeleiteten Klasse die gleichnamige Methode der Basisklasse verbergen soll,<br />

ohne dass Änderungen am Code der Basisklasse oder an anderen Programmteilen,<br />

die diese Basisklasse verwenden, notwendig wären. Der folgende Code zeigt<br />

den Einsatz von new:<br />

Listing 5.8:<br />

Verbergen der Methode der Basisklasse<br />

1: class BaseClass<br />

2: {<br />

3: public void TestMethod()<br />

4: {<br />

5: Console.WriteLine("BaseClass::TestMethod");<br />

6: }<br />

7: }<br />

8:<br />

9: class DerivedClass:BaseClass<br />

10: {<br />

11: new public void TestMethod()<br />

12: {<br />

13: Console.WriteLine("DerivedClass::TestMethod");<br />

14: }<br />

15: }<br />

Wohlgemerkt: Eine mit new in einer Ableitung deklarierte Methode kommt nur<br />

dann zum Einsatz, wenn Sie die Objekte auch über die abgeleitete Klasse ansprechen:<br />

DerivedClass newtest = new DerivedClass();<br />

BaseClass oldtest = new DerivedClass();<br />

newtest.TestMethod();// = DerivedClass.TestMethod<br />

oldtest.TestMethod();// = BaseClass.TestMethod<br />

((BaseClass)newtest).TestMethod();// = BaseClass.TestMethod<br />

Dieses Verhalten unterscheidet sich grundlegend von Methoden, die in der Basisklasse<br />

als virtual deklariert und in der abgeleiteten Klasse mit override überschrieben<br />

werden. Dort ist garantiert, dass die jeweils »neueste« Variante einer<br />

Methode aufgerufen wird.


Kapitel 5 • Klassen 77<br />

5.3 Eigenschaften<br />

Es gibt in C# zwei Möglichkeiten, benannte Attribute von Klassen öffentlich verfügbar<br />

zu machen: als Datenelemente oder als Eigenschaften. Bei Datenelementen<br />

geht es schlicht um wertbehaftete Variablen, die über den Modifizierer public<br />

als öffentlich ausgewiesen werden. Eigenschaften haben dagegen nur indirekt<br />

etwas mit Speicherbereichen zu tun, weil sich hinter ihnen Zugriffsfunktionen verbergen.<br />

Zugriffsfunktionen (Accessors) legen die Anweisungen fest, die eine Klasse beim<br />

lesenden bzw. schreibenden Zugriff auf eine Eigenschaft ausführt. Lesende<br />

Zugriffsfunktionen werden durch das Schlüsselwort get gekennzeichnet, schreibende<br />

Zugriffsfunktionen durch das Schlüsselwort set.<br />

Bevor die Theorie wieder zu grau wird, ein praktisches Beispiel in Form einer<br />

Klasse House mit einer Eigenschaft SquareFeet, die sich sowohl lesen als auch<br />

(von außerhalb dieser Klasse) neu setzen lässt:<br />

Listing 5.9:<br />

Implementation von Zugriffsfunktionen<br />

1: using System;<br />

2:<br />

3: public class House<br />

4: {<br />

5: private int m_nSqFeet;<br />

6:<br />

7: public int SquareFeet<br />

8: {<br />

9: get { return m_nSqFeet; }<br />

10: set { m_nSqFeet = value; }<br />

11: }<br />

12: }<br />

13:<br />

14: class TestApp<br />

15: {<br />

16: public static void Main()<br />

17: {<br />

18: House myHouse = new House();<br />

19: myHouse.SquareFeet = 250;<br />

20: Console.WriteLine(myHouse.SquareFeet);<br />

21: }<br />

22: }<br />

Der aktuelle Wert der Eigenschaft SquareFeet wird hier in einem privaten Feld<br />

(m_nSqFeet) gespeichert. Wenn Sie aus SquareFeet eine gewöhnliche öffentliche<br />

Elementvariable machen wollen würden, dann müssten Sie lediglich die Zugriffsfunktionen<br />

streichen und nur die Variablendeklaration übrig lassen:


78<br />

Indizierer<br />

public int SquareFeet;<br />

Für einfache Variablen ist das auch durchaus in Ordnung. Wenn Sie allerdings<br />

Details über die innere Speicherstruktur Ihrer Klasse verbergen wollen, dann sollten<br />

Sie Zugriffsfunktionen verwenden. Die set-Funktion bekommt den neuen<br />

Wert als Parameter value übergeben (wobei dieser Name fix vorgegeben ist –<br />

siehe Zeile 10 des Listings).<br />

Der Hauptzweck von Zugriffsfunktionen liegt natürlich nicht im Verbergen innerer<br />

Speicherstrukturen, sondern zum einen in der Möglichkeit, Lese- oder<br />

Schreibzugriffe explizit zu sperren:<br />

• get implementiert, set nicht: Die Eigenschaft kann außerhalb der Klassendefinition<br />

nur gelesen, nicht aber verändert werden.<br />

• set implementiert, get nicht: Die Eigenschaft kann außerhalb der Klassendefinition<br />

nur geschrieben, nicht aber wieder gelesen werden.<br />

• get und set implementiert: Der Wert der Eigenschaft lässt sich außerhalb der<br />

Klassendefinition sowohl lesen als auch verändern.<br />

Der andere und mindestens genauso wichtige Vorteil: set kann selbstverständlich<br />

Prüfungen enthalten und beispielsweise dafür sorgen, dass das Haus nicht versehentlich<br />

eine negative Quadratmeterzahl erhält; hinter get kann sich ebenso gut<br />

eine dynamische Eigenschaft verstecken – mithin ein Wert, der erst auf Anforderung<br />

berechnet und dann eventuell auch noch verifiziert wird. Eine Klasse House<br />

könnte bei Leseanforderungen beispielsweise die Quadratmeterzahlen sämtlicher<br />

in einer eigenen Liste gehaltenen Räume zusammenrechnen – und eine Klasse<br />

Picture ein Bild erst dann laden, wenn das Programm die Daten tatsächlich benötigt.<br />

5.4 Indizierer<br />

Wollten Sie Ihre Klassen nicht schon immer einmal auf einfachste Weise mit indizierten<br />

Zugriffen ausstatten – wie bei den Elementen eines Arrays? Mit C# geht<br />

das praktisch ohne Drumherum. Die grundlegende Syntax sieht so aus:<br />

Attribute Modifizierer Deklarator { Deklarationen }<br />

Ein einfaches Beispiel für eine Implementation:<br />

public string this[int nIndex]<br />

{<br />

get { ... }<br />

set { ... }<br />

}


Kapitel 5 • Klassen 79<br />

Dieser Indizierer liest bzw. schreibt einen Zeichenkette mit einem bestimmten<br />

Index. Attribute hat er keine, verwendet aber den Modifizierer public. Der Deklaratorteil<br />

besteht aus string als Typangabe und this für den Indizierer der Klasse.<br />

Die Implementationsregeln für get und set sind dieselben wie bei Eigenschaften<br />

(was bedeutet, dass Sie Zugriffe wahlweise auf Lesen oder Schreiben einschränken<br />

können). Einen Unterschied gibt es allerdings: Bei der in eckigen Klammern<br />

anzugebenden Parameterliste haben Sie praktisch freie Wahl. Die einzigen Grenzen<br />

sind hier, dass diese Liste mindestens einen Parameter enthalten muss und<br />

sich die Modifizierer ref sowie out nicht verwenden lassen.<br />

Indizierer haben keine eigenen Namen. Das Schlüsselwort this steht für den Indizierer<br />

der Schnittstelle, die die Klasse implementiert. Für Klassen, die mehrere<br />

Schnittstellen implementieren (also von mehreren Schnittstellen abgeleitet sind),<br />

kann hier der Name der Schnittstelle (in der Form Schnittstellenname.this) angegeben<br />

werden, für die der Indizierer zuständig ist.<br />

Das folgende Listing demonstriert den Einsatz eines Indizierers anhand einer einfachen<br />

Klasse, die einen Hostnamen zu einer IP-Adresse auflöst – oder eben bei<br />

Hostnamen wie www.microsoft.com zu einer ganzen Liste von IP-Adressen, die<br />

dann über den Indizierer zugänglich sind.<br />

Listing 5.10: Abfrage von IP-Adressen über einen Indizierer<br />

1: using System;<br />

2: using System.Net;<br />

3:<br />

4: class ResolveDNS<br />

5: {<br />

6: IPAddress[] m_arrIPs;<br />

7:<br />

8: public void Resolve(string strHost)<br />

9: {<br />

10: IPHostEntry iphe = DNS.GetHostByName(strHost);<br />

11: m_arrIPs = iphe.AddressList;<br />

12: }<br />

13:<br />

14: public IPAddress this[int nIndex]<br />

15: {<br />

16: get<br />

17: {<br />

18: return m_arrIPs[nIndex];<br />

19: }<br />

20: }<br />

21:<br />

22: public int Count<br />

23: {<br />

24: get { return m_arrIPs.Length; }


80<br />

Ereignisse<br />

25: }<br />

26: }<br />

27:<br />

28: class DNSResolverApp<br />

29: {<br />

30: public static void Main()<br />

31: {<br />

32: ResolveDNS myDNSResolver = new ResolveDNS();<br />

33: myDNSResolver.Resolve("www.microsoft.com");<br />

34:<br />

35: int nCount = myDNSResolver.Count;<br />

36: Console.WriteLine("{0} IP-Adressen für {1} gefunden:", nCount,<br />

"www.microsoft.com");<br />

37: for (int i=0; i < nCount; i++)<br />

38: Console.WriteLine(myDNSResolver[i]);<br />

39: }<br />

40: }<br />

Die zur Auflösung des Hostnamens verwendete Klasse DNS ist Teil des Namensraums<br />

System.Net. Da dieser Namensraum nicht in der NGWS-Kernbibliothek<br />

enthalten ist, muss die entsprechende Bibliothek explizit beim Aufruf des Compilers<br />

angegeben werden:<br />

csc /r:System.Net.dll /out:resolver.exe resolver.cs<br />

Die Logik des Programms ist recht geradlinig: In der Methode Resolve wird die<br />

statische Methode GetHostByName der Klasse DNS aufgerufen, die ein IPHost-<br />

Entry-Objekt zurückgibt. Dieses Objekt enthält das Array AddressList, dessen<br />

Elemente IPAddress-Objekte sind, und um das es in diesem Beispiel geht:<br />

AddressList wird von Resolve in der objekteigenen Elementvariablen m_arrIPs<br />

gespeichert.<br />

Nachdem das Array nun besetzt ist, kann die Anwendung die IP-Adressen über<br />

den Indizierer der Klasse ResolveDNS abzählen (Zeilen 37 und 38 im Listing).<br />

Dieser Indizierer implementiert nur eine get-Methode, weil das Verändern der IP-<br />

Adressen nicht sonderlich sinnvoll wäre. Details zu der verwendeten for-Schleife<br />

finden Sie übrigens in Kapitel 6, »Kontrollstrukturen«.<br />

5.5 Ereignisse<br />

Bei Klassen ergibt sich des Öfteren die Notwendigkeit, dass ihre Objekte sich bei<br />

anderen Teilen einer Anwendung (oder anderen Objekten, was in C# dasselbe ist)<br />

sozusagen aus eigenem Antrieb melden müssen. Wenn Sie Erfahrungen mit anderen<br />

Programmiersprachen gesammelt haben, wird Ihnen die Vielzahl der Wege<br />

geläufig sein, über die man solche Rückmeldungen zu Stande bringen kann – bei-


Kapitel 5 • Klassen 81<br />

spielsweise mit Zeigern auf Rückruffunktionen oder den Event Sinks von<br />

ActiveX-Steuerelementen. C# stellt mit Ereignissen einen eigenen Mechanismus<br />

für solche Benachrichtigungen zur Verfügung.<br />

Ereignisse lassen sich wahlweise als Elementvariablen oder als Eigenschaften<br />

einer Klasse definieren. Beiden Verfahren ist gemeinsam, dass der Typ eines<br />

Ereignisses mit delegate gekennzeichnet sein muss – dem Schlüsselwort, das in<br />

C# das Äquivalent zu Prototypen von Funktionszeigern darstellt.<br />

Ereignisse können von einer beliebigen Zahl von Clients konsumiert werden (sind<br />

im Gegensatz zu den klassischen Funktionszeigern also nicht auf einen einzigen<br />

Abnehmer beschränkt); Clients können sich zu jedem Zeitpunkt mit einem Ereignis<br />

verbinden bzw. davon wieder lösen. Delegationen lassen sich sowohl als<br />

statische als auch als auf die jeweilige Objektinstanz bezogene Methoden implementieren,<br />

wobei die zweite Möglichkeit vor allem C++-Programmierern entgegenkommt.<br />

Nachdem nun nicht nur alle wesentlichen Features von Ereignissen, sondern auch<br />

die der dazugehörigen Delegationen erwähnt worden sind, folgt anstelle weiterer<br />

Theorie ein praktisches Beispiel.<br />

Listing 5.11: Implementation einer Ereignisbehandlung<br />

1: using System;<br />

2:<br />

3: // forward-Deklaration<br />

4: public delegate void EventHandler(string strText);<br />

5:<br />

6: class EventSource<br />

7: {<br />

8: public event EventHandler TextOut;<br />

9:<br />

10: public void TriggerEvent()<br />

11: {<br />

12: if (null != TextOut) TextOut("Ereignis ausgelöst");<br />

13: }<br />

14: }<br />

15:<br />

16: class TestApp<br />

17: {<br />

18: public static void Main()<br />

19: {<br />

20: EventSource evsrc = new EventSource();<br />

21:<br />

22: evsrc.TextOut += new EventHandler(CatchEvent);<br />

23: evsrc.TriggerEvent();<br />

24:<br />

25: evsrc.TextOut -= new EventHandler(CatchEvent);


82<br />

Ereignisse<br />

26: evsrc.TriggerEvent();<br />

27:<br />

28: TestApp theApp = new TestApp();<br />

29: evsrc.TextOut += new EventHandler(theApp.InstanceCatch);<br />

30: evsrc.TriggerEvent();<br />

31: }<br />

32:<br />

33: public static void CatchEvent(string strText)<br />

34: {<br />

35: Console.WriteLine(strText);<br />

36: }<br />

37:<br />

38: public void InstanceCatch(string strText)<br />

39: {<br />

40: Console.WriteLine("Instance " + strText);<br />

41: }<br />

42: }<br />

In Zeile 4 dieses Programms wird die Delegation (also der Prototyp der Ereignisbehandlungsroutine)<br />

deklariert. Diesen Typ verwendet die Klasse EventSource<br />

für das Element TextOut in Zeile 8. (Tatsächlich kann man die Deklaration von<br />

Delegationen als Deklaration eines speziellen Typs sehen, der dann seinerseits für<br />

die Deklaration von Ereignissen verwendbar ist.)<br />

Die Klasse EventSource hat lediglich eine Methode, die zum Auslösen des Ereignisses<br />

dient. Bitte beachten Sie, dass TriggerEvent das Ereignis TextOut erst einmal<br />

prüfen muss: Wenn dort der Wert null steht, bedeutet das, dass sich zu diesem<br />

Zeitpunkt niemand für dieses Ereignis interessiert.<br />

Die Klasse TestApp beherbergt in diesem Beispiel neben Main zwei Methoden<br />

mit einer zu dem Ereignis passenden Signatur. Die eine (CatchEvent) ist statisch,<br />

bezieht sich also auf die Klasse, die andere ist speizifisch für das jeweilige Test-<br />

App-Objekt.<br />

Nach dem Anlegen eines EventSource-Objekts meldet Main erst einmal die statische<br />

Methode bei dem TextOut-Ereignis an:<br />

evsrc.TextOut += new EventHandler(CatchEvent);<br />

Im Weiteren wird also die TestApp-Methode CatchEvent als Reaktion auf das<br />

Ereignis aufgerufen. Wenn das zu irgend einem Zeitpunkt nicht weiter gewünscht<br />

sein sollte, dann ist eine Abmeldung fällig, die so aussieht:<br />

evsrc.TextOut -= new EventHandler(CatchEvent);<br />

Das An- und Abmelden von Methoden für die Ereignisbehandlung ist ausschließlich<br />

innerhalb der Klasse möglich, die die jeweiligen Methoden selbst definiert.


Kapitel 5 • Klassen 83<br />

(Die Ereignisbehandlung einer anderen Klasse können Sie in TestApp also nicht<br />

verändern.)<br />

Der zweite Teil von Main demonstriert schließlich, dass sich Ereignisse auch von<br />

Methoden behandeln lassen, die auf Objektvariablen einer Klasse bezogen sind:<br />

Er legt ein Objekt der Klasse TestApp an und registriert die Methode Instance-<br />

Catch dieses Objekts.<br />

Was man mit Ereignissen und Delegationen in der Praxis anfangen kann, wird<br />

nicht nur von ASP+, sondern auch von den Windows Foundation Classes in aller<br />

Ausführlichkeit demonstriert.<br />

5.6 Modifizierer<br />

In den vorangehenden Abschnitten dieses Kapitels haben Sie bereits einige Modifizierer<br />

wie public und virtual zu Gesicht bekommen. Auf den folgenden Seiten<br />

geht es nun um eine vollständige Betrachtung, die der Übersichtlichkeit halber in<br />

drei Abschnitte unterteilt ist:<br />

• Modifizierer für Klassen<br />

• Modifizierer für Elemente<br />

• Modifizierer für die Verfügbarkeit<br />

5.6.1 Modifizierer für Klassen<br />

Bei Klassen haben sich die bis dato vorgestellten Beispiele auf Modifizierer für<br />

die Verfügbarkeit beschränkt. C# definiert allerdings zwei Modifizierer, die ausschließlich<br />

auf Klassen anwendbar sind:<br />

• abstract – für Klassen, deren wesentliche Eigenschaft daraus besteht, dass<br />

man sie ausschließlich als Basis für weitere Klassen, nicht aber zum Anlegen<br />

von Objekten verwenden kann. Ableitungen einer abstrakten Klasse, von denen<br />

sich Objekte anlegen lassen sollen, müssen alle in der Basisklasse als abstrakt<br />

gekennzeichneten Elemente implementieren.<br />

• sealed – für Klassen, von denen sich keine weiteren Klassen ableiten lassen.<br />

Dieser Modifizierer wird von einigen Klassen des NGWS-Programmiergerüsts<br />

verwendet und ist nicht mit abstract kombinierbar.<br />

Das folgende Beispiel demonstriert den Einsatz dieser beiden Modifizierer: Es<br />

leitet eine versiegelte Klasse von einer abstrakten Klasse ab (was man in der Praxis<br />

wohl nur selten auf derart direktem Wege machen wird).


84<br />

Modifizierer<br />

Listing 5.12: Abstrakte und versiegelte Klassen<br />

1: using System;<br />

2:<br />

3: abstract class AbstractClass<br />

4: {<br />

5: abstract public void MyMethod();<br />

6: }<br />

7:<br />

8: sealed class DerivedClass:AbstractClass<br />

9: {<br />

10: public override void MyMethod()<br />

11: {<br />

12: Console.WriteLine("versiegelte Klasse ");<br />

13: }<br />

14: }<br />

15:<br />

16: public class TestApp<br />

17: {<br />

18: public static void Main()<br />

19: {<br />

20: DerivedClass dc = new DerivedClass();<br />

21: dc.MyMethod();<br />

22: }<br />

23: }<br />

5.6.2 Modifizierer für Elemente<br />

Bei Elementen bietet C# eine erheblich größere Auswahl von Modifizierern als<br />

bei Klassen. Einige dieser Modifizierer wurden bereits vorgestellt, der Rest wird<br />

im weiteren Verlauf dieses Buchs anhand praktischer Beispiele erläutert:<br />

• abstract – für Methoden und Zugriffsfunktionen, die in der jeweiligen Klassendeklaration<br />

keine Implementation haben. Auf diese Weise deklarierte Elemente<br />

sind implizit virtual und müssen bei der Implementation in abgeleiteten<br />

Klassen mit override gekennzeichnet werden.<br />

• const – für wertbehaftete Elemente und lokale Variablen. Auf diese Weise gekennzeichnete<br />

Ausdrücke werden zum Zeitpunkt der Kompilierung ausgewertet,<br />

weshalb sie keine Referenzen auf Variablen enthalten können.<br />

• event – für die Deklaration von Variablen und Eigenschaften als Ereignis.<br />

Über Elemente dieses Typs werden Clients mit Ereignissen von Klassen und<br />

Objektvariablen verbunden.<br />

• extern – teilt dem Compiler mit, dass eine Methode an anderer Stelle als im<br />

aktuellen Quelltext implementiert ist. Details dazu finden Sie in Kapitel 10,<br />

»Integration von nicht verwaltetem Code«.


Kapitel 5 • Klassen 85<br />

• override – zum Überschreiben von Methoden und Zugriffsfunktionen, die in<br />

einer zugrunde liegenden Klasse als virtual deklariert sind. Die Signaturen<br />

(also Parameterzahl, -typen und -reihenfolge) dieser Methode bzw. Zugriffsfunktion<br />

und ihres Ersatzes müssen identisch sein.<br />

• readonly – der Wert eines mit diesem Modifizierer deklarierten Elements lässt<br />

sich nur im Rahmen der Deklaration sowie im Konstruktor der Klasse ändern.<br />

• static – für Elemente, die zur Klasse selbst (und nicht zu Objekten der Klasse)<br />

gehören. Dieser Modifizierer ist für wertbehaftete Variablen, Methoden, Eigenschaften,<br />

Operatoren und nicht zuletzt für Konstruktoren verwendbar.<br />

• virtual-für Methoden und Zugriffsfunktionen, die sich in Ableitungen der<br />

Klasse ersetzen lassen.<br />

5.6.3 Modifizierer für die Verfügbarkeit<br />

Diese Modifizierer legen fest, auf welcher Ebene Elemente einer Klasse (wie<br />

wertbehaftete Felder, Methoden und Eigenschaften) verfügbar sind. Elemente, die<br />

nicht explizit mit einem solchen Modifizierer gekennzeichnet sind, stehen öffentlich<br />

zur Verfügung.<br />

Die folgenden Modifizierer legen die Verfügbarkeit fest:<br />

• public – das Element ist sowohl für Code innerhalb als auch außerhalb der<br />

Klasse verfügbar. Dieser Modifizierer stellt die niedrigste Stufe der Einschränkung<br />

dar.<br />

• protected – das Element ist für Code innerhalb der Klasse und für Code innerhalb<br />

aller Ableitungen dieser Klasse verfügbar. Zugriffe von außerhalb der<br />

Klasse und ihrer Ableitungen sind nicht möglich.<br />

• private – das Element ist ausschließlich für Code innerhalb der Klasse verfügbar,<br />

also weder für Code außerhalb der Klasse noch für Code innerhalb von<br />

Ableitungen.<br />

• internal – das Element ist für Code innerhalb derselben NGWS-Komponente<br />

(Anwendung oder Bibliothek) verfügbar, nicht aber für Code außerhalb. Diese<br />

Einschränkung ließe sich als public auf der NGWS-Komponentenebene und<br />

private für den Rest der Welt bezeichnen.<br />

Das folgende Listing demonstriert diese Modifizierer. Es ist gegenüber dem vorangehenden<br />

Beispiel lediglich um einige Elementvariablen und eine abgeleitete<br />

Klasse erweitert.


86<br />

Modifizierer<br />

Listing 5.13: Modifizierer für die Verfügbarkeit<br />

1: using System;<br />

2:<br />

3: internal class Triangle<br />

4: {<br />

5: protected int m_a, m_b, m_c;<br />

6: public Triangle(int a, int b, int c)<br />

7: {<br />

8: m_a = a;<br />

9: m_b = b;<br />

10: m_c = c;<br />

11: }<br />

12:<br />

13: public virtual double Area()<br />

14: {<br />

15: // Heronsche Formel<br />

16: double s = (m_a + m_b + m_c) / 2.0;<br />

17: double dArea = Math.Sqrt(s*(s-m_a)*(s-m_b)*(s-m_c));<br />

18: return dArea;<br />

19: }<br />

20: }<br />

21:<br />

22: internal class Prism:Triangle<br />

23: {<br />

24: private int m_h;<br />

25: public Prism(int a, int b, int c, int h):base(a,b,c)<br />

26: {<br />

27: m_h = h;<br />

28: }<br />

29:<br />

30: public override double Area()<br />

31: {<br />

32: double dArea = base.Area() * 2.0;<br />

33: dArea += m_a*m_h + m_b*m_h + m_c*m_h;<br />

34: return dArea;<br />

35: }<br />

36: }<br />

37:<br />

38: class PrismApp<br />

39: {<br />

40: public static void Main()<br />

41: {<br />

42: Prism prism = new Prism(2,5,6,1);<br />

43: Console.WriteLine(prism.Area());<br />

44: }<br />

45: }


Kapitel 5 • Klassen 87<br />

Die Klassen Triangle und Prism sind hier beide als internal deklariert, also ausschließlich<br />

innerhalb der aktuellen NGWS-Komponente verfügbar. Bitte beachten<br />

Sie, dass der Begriff NGWS-Komponente für die Zusammenfassung von Klassen<br />

zu Modulen steht, also nicht im Sinne von COM+-Komponenten gemeint ist. Die<br />

Klasse Triangle hat drei als protected deklarierte Elemente, die im Konstruktor<br />

initialisiert und in der Methode Area verwendet werden – sowohl in der Basisklassenvariante<br />

als auch in der durch die abgeleitete Klasse überschriebenen Version<br />

dieser Methode. Die Klasse Prism deklariert ein Feld m_h als private, das<br />

ausschließlich in dieser Klasse (also auch nicht in eventuellen weiteren Ableitungen<br />

davon) verfügbar ist.<br />

Je mehr gründliche Überlegung Sie beim Design von Klassen dem Thema Verfügbarkeit<br />

widmen, desto einfacher können später eventuell notwendige Modifikationen<br />

ausfallen. Ein Element, das ohne viel Federlesens von vornherein als<br />

public deklariert worden ist, dürfte sich nachträglich nur noch schwerlich an veränderte<br />

Bedürfnisse anpassen lassen – ganz im Gegensatz zu Teilen einer Klasse,<br />

die per se ausschließlich im Verborgenen arbeiten. Das gilt nicht nur für Elemente<br />

von Klassen, sondern auch für die Klassen selbst.<br />

5.7 Zusammenfassung<br />

In diesem Kapitel ging es um die verschiedenen Bestandteile von Klassen, die<br />

ihrerseits die Vorlage für Objekte darstellen. Die im Konstruktor einer Klasse<br />

festgelegten Anweisungen werden beim Anlegen von Objekten einer Klasse als<br />

Erstes ausgeführt und übernehmen üblicherweise die Initialisierung wertbehafteter<br />

Elemente, die sich ihrerseits später von Methoden der Klasse einsetzen lassen.<br />

Methoden können Werte als Parameter übernehmen, Referenzen an Variablen<br />

weiterreichen oder sich auch nur auf das Zurückgeben von Ausgangsparametern<br />

beschränken. Sie lassen sich zur Implementation neuer oder veränderter Funktionalität<br />

überschreiben; die Alternative ist das Verbergen einer gleichnamigen<br />

Methode einer Basisklasse.<br />

Benannte Attribute einer Klasse lassen sich sowohl über wertbehaftete Elementvariablen<br />

als auch über Eigenschaften mit entsprechenden Zugriffsfunktionen<br />

implementieren. Zugriffsfunktionen werden über die Schlüsselwörter get und set<br />

gekennzeichnet und ermöglichen unter anderem die Prüfung neu zu setzender<br />

bzw. zurückzugebender Werte. Eigenschaften, die nur eine der beiden Zugriffsfunktionen<br />

implementieren, lassen sich außerhalb der Klasse ausschließlich lesen<br />

bzw. schreiben.<br />

Ein weiteres Feature von C#-Klassen stellen Indizierer dar: Sie erlauben den<br />

Zugriff auf Werte über dieselbe Syntax, wie sie bei Arrays zum Einsatz kommt.


88<br />

Zusammenfassung<br />

Des Weiteren definiert C# einen Mechanismus zur Signalisierung von und Reaktion<br />

auf Ereignisse. Klassen und ihre Objekte können bei Bedarf eine beliebige<br />

Zahl von Clients über Zustandsänderungen benachrichtigen.<br />

Die Lebensspanne eines Objekts endet mit dem Aufruf seines Destruktors durch<br />

den Garbage Collector. Da sich nicht exakt bestimmen lässt, zu welchem Zeitpunkt<br />

der Garbage Collector zum Zuge kommt, sollten Sie gegebenenfalls eine<br />

Methode zur Freigabe von Ressourcen definieren, die ein Programm dann direkt<br />

aufruft, wenn es ein Objekt nicht länger braucht.


Kapitel 6<br />

Kontrollstrukturen<br />

6.1 Bedingte Ausführung 90<br />

6.2 Schleifen 97<br />

6.3 Zusammenfassung 103


90<br />

Bedingte Ausführung<br />

Eine spezielle Kategorie von Anweisungen werden Sie in jeder Programmiersprache<br />

finden: Kontrollstrukturen. Dieses Kapitel stellt Ihnen die Kontrollstrukturen<br />

der Sprache C# vor, die sich wie üblich in zwei Gruppen unterteilen lassen:<br />

• Anweisungen für die bedingte Ausführung von Code<br />

• Anweisungen für Schleifen<br />

Wenn Ihnen C oder C++ geläufig ist, wird Ihnen vieles in diesem Kapitel recht<br />

bekannt vorkommen. Seien Sie in diesem Fall aber dennoch wachsam – es gibt<br />

einige Unterschiede.<br />

6.1 Bedingte Ausführung<br />

Kontrollstrukturen für die bedingte Ausführung sind Anweisungen, die anhand<br />

eines Wertes entscheiden, welcher Codezweig zur Ausführung kommen soll. C#<br />

kennt zwei Anweisungen dieser Art: if und switch.<br />

6.1.1 if<br />

Die allgemeinere der beiden Anweisungen ist die if-Anweisung. Sie führt die<br />

Anweisung bedingteAnweisung nur dann aus, wenn sich als Wahrheitswert für<br />

die logische Bedingung boolescherAusdruck der Wert true ergibt:<br />

if (boolescherAusdruck) bedingteAnweisung<br />

Natürlich lässt sich in C# auch ein else-Zweig angeben, der dann zur Ausführung<br />

kommt, wenn die Bedingung den Wahrheitswert false liefert:<br />

if (boolescherAusdruck) bedingteAnweisung else bedingteAnweisung<br />

Um beispielsweise sicherzustellen, dass die Zeichenfolge strTest nicht leer ist,<br />

schreiben Sie:<br />

if (0 != strTest.Length)<br />

{<br />

}<br />

Der für die Bedingung verwendete Vergleichsoperator != hat die Bedeutung: »ist<br />

ungleich«. Als C/C++-ProgrammiererIn werden Sie wahrscheinlich Code wie<br />

diesen gewohnt sein:<br />

if (strTest.Length)<br />

{<br />

}


Kapitel 6 • Kontrollstrukturen 91<br />

In C# bedenkt der Compiler solche Formulierungen mit der Fehlermeldung:<br />

error CS0029: Cannot implicitly convert type 'int' to 'bool'<br />

Das liegt daran, dass die if-Anweisung für die Bedingung den Datentyp bool<br />

erwartet und die Length-Eigenschaft des string-Objekts den Typ int trägt – eine<br />

implizite Typumwandlung von int nach bool ist in C#, wie bereits erwähnt, nicht<br />

definiert.<br />

Vielleicht werden Sie es als Nachteil empfinden, eine liebgewordene Programmiergewohnheit<br />

aufgeben zu müssen. Der Vorteil ist aber, dass Sie so nie wieder<br />

der berüchtigten »Zuweisung in der if-Bedingung« auf den Leim gehen werden:<br />

if (nMyValue = 5) ...<br />

Damit eine Vergleichsoperation daraus wird, sollte der Code eigentlich so lauten:<br />

if (nMyValue == 5) ...<br />

Und nur diese Formulierung lässt der C#-Compiler auch durchgehen. Die weiteren,<br />

den Sprachen C und C++ entlehnten, Vergleichsoperatoren von C# sind:<br />

• == – liefert true, wenn beide Operanden übereinstimmen<br />

• != – liefert true, wenn beide Operanden nicht übereinstimmen<br />

• = – liefert true, wenn die Operanden die entsprechende Relation erfüllen<br />

(»kleiner als«, »kleiner gleich«, »größer als« und »größer gleich«).<br />

Erwartungsgemäß können Sie mit diesen Operatoren nicht jeden Datentyp mit<br />

jedem Datentyp vergleichen. Da die einzelnen Datentypen jeweils ihre eigenen<br />

überladenen Varianten für diese Operatoren definieren (oder auch nicht), ist man<br />

für typübergreifende Vergleiche – etwa eines int-Werts mit einem double-Wert –<br />

auf die implizite Typumwandlung durch den Compiler angewiesen. Wenn das<br />

nicht greift, bleibt noch das Mittel der expliziten Typumwandlung – doch dann<br />

sollte man sich schon genau darüber im Klaren sein, was passiert, wenn man<br />

»Äpfel mit als Äpfel getarnten Birnen« vergleicht.<br />

Das folgende Listing zeigt verschiedene Szenarien für den Einsatz der if-Anweisung<br />

und demonstriert auch recht schön den Umgang mit dem Datentyp string.<br />

Der Code wertet den ersten Kommandozeilenparameter der Anwendung daraufhin<br />

aus, ob er mit einem großen oder kleinen Buchstaben oder einer Ziffer<br />

beginnt.<br />

Listing 6.1:<br />

1: using System;<br />

2:<br />

3: class NestedIfApp<br />

4: {<br />

Ein einzelnes Zeichen einer Zeichenfolge untersuchen


92<br />

Bedingte Ausführung<br />

5: public static int Main(string[] args)<br />

6: {<br />

7: if (args.Length != 1)<br />

8: {<br />

9: Console.WriteLine("Aufruf: Ein Kommandozeilenargument<br />

angeben");<br />

10: return 1; // Fehlernummer<br />

11: }<br />

12:<br />

13: char chLetter = args[0][0];<br />

14:<br />

15: if (chLetter >= 'A')<br />

16: if (chLetter = 'a' && chLetter


Kapitel 6 • Kontrollstrukturen 93<br />

auswertet, wenn sich der Wert des Ausdrucks bereits aus dem ersten Operanden<br />

ergibt. Falls also der erste Operand in einer UND-Operation den Wert false hat,<br />

ist das Ergebnis des Ausdrucks in jedem Fall false, und falls er in einer ODER-<br />

Operation den Wert true hat, ist das Ergebnis des Ausdrucks in jedem Fall true.<br />

Vom Prinzip her können Sie also Laufzeit einsparen, indem Sie den Operanden<br />

zuerst notieren, der die Operation voraussichtlich bereits allein entscheiden kann.<br />

Leider hat diese Form der Optimierung auch ihren Preis: Sie können sich nicht<br />

darauf verlassen, dass der zweite Operand ausgewertet wird – was in der Praxis zu<br />

schwer auffindbaren Fehlern führen kann, wenn diese Auswertung den Wert einer<br />

Variablen ändert. Das folgende Beispiel stellt die Situation übertrieben augenfällig<br />

dar:<br />

if (1 == 1 || (5 == (strLength=str.Length)))<br />

{<br />

Console.WriteLine(strLength);<br />

}<br />

Da der erste Operand in jedem Fall true ist, kommt die im zweiten Operanden<br />

versteckte Zuweisung nie zur Ausführung. Die Praxis hält subtilere Fallen bereit<br />

– etwa, wenn als zweiter Operand eine Methode zum Aufruf kommt, die den Wert<br />

eines Referenzparameters oder globalen Variable verändert.<br />

if (Test(lNumber) || TestAndAdd1(lNumber))<br />

{<br />

}<br />

6.1.2 switch<br />

Im Gegensatz zur if-Anweisung entscheidet bei der switch-Anweisung ein Steuerausdruck<br />

darüber, welcher von mehreren möglichen Anweisungszweigen zur<br />

Ausführung kommt. Dazu sind den einzelnen Anweisungszweigen (je unterschiedliche)<br />

Fallbeschreibungen in Form konstanter Werte zugeordnet. Die allgemeine<br />

Syntax der switch-Anweisung lautet:<br />

switch (Steuerausdruck)<br />

{<br />

case konstanterAusdruck:<br />

Anweisungszweig<br />

default:<br />

Anweisungszweig<br />

}<br />

Als Datentypen für Steuerausdruck sind sbyte, byte, short, ushort, uint, long,<br />

ulong, char, string sowie alle Aufzählungstypen erlaubt. Weiterhin kommen<br />

auch Typen in Frage, für die der Compiler eine implizite Typumwandlung in<br />

einen dieser Typen vornehmen kann.


94<br />

Bedingte Ausführung<br />

Für die Abarbeitung der switch-Anweisung gilt folgende Reihenfolge:<br />

1. Als Erstes wird der Ausdruck Steuerausdruck ausgewertet.<br />

2. Stimmt der Wert von Steuerausdruck mit konstanterAusdruck eines<br />

Zweigs überein, kommt es zur Ausführung der in dem Zweig enthaltenen Anweisungen.<br />

3. Stimmt der Wert des Steuerausdrucks mit keiner Fallbeschreibung überein,<br />

kommt es zur Ausführung der im default-Zweig enthaltenen Anweisungen –<br />

sofern ein solcher definiert ist; andernfalls wird der switch-Block übersprungen,<br />

ohne dass es zur Ausführung irgendeines Zweiges kommt.<br />

Vor der Diskussion weiterer Details der switch-Kontrollstruktur sollten Sie sich<br />

das Programm in Listing 6.2 ansehen. Es arbeitet mit einer switch-Anweisung,<br />

die sechs Fälle unterscheidet, um für einen gegebenen Monat die Anzahl der Tage<br />

(ohne Berücksichtigung von Schaltjahren) zu ermitteln.<br />

Listing 6.2:<br />

Eine switch-Anweisung unterscheidet, welcher Monat wie viele Tage<br />

hat<br />

1: using System;<br />

2:<br />

3: class FallThrough<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: if (args.Length != 1) return;<br />

8:<br />

9: int nMonth = Int32.Parse(args[0]);<br />

10: if (nMonth < 1 || nMonth > 12) return;<br />

11: int nDays = 0;<br />

12:<br />

13: switch (nMonth)<br />

14: {<br />

15: case 2: nDays = 28; break;<br />

16: case 4:<br />

17: case 6:<br />

18: case 9:<br />

19: case 11: nDays = 30; break;<br />

20: default: nDays = 31;<br />

21: }<br />

22: Console.WriteLine("Der Monat hat {0} Tage.",nDays);<br />

23: }<br />

24: }<br />

Der switch-Block umfasst die Zeilen 13 bis 21. C/C++-ProgrammiererInnen wird<br />

der Code recht vertraut vorkommen, nicht zuletzt deshalb, weil er break-Anweisungen<br />

enthält. Dennoch, es gibt einen Unterschied, der einem in C# Fehler ver-


Kapitel 6 • Kontrollstrukturen 95<br />

meiden hilft. Wann immer ein Zweig Anweisungen enthält, muss er durch eine<br />

break-Anweisung (oder eine goto-Anweisung zu einem anderen Zweig) abgeschlossen<br />

sein, ansonsten meldet der Compiler einen Fehler. Das Durchlaufen<br />

mehrerer Zweige aufgrund einer fehlenden break-Anweisung wie in C/C++ ist<br />

also nicht möglich. Der folgende Code würde von einem C/C++-Compiler akzeptiert<br />

– bei C# kommt dagegen eine Fehlermeldung heraus:<br />

nVar = 1<br />

switch (nVar)<br />

{<br />

case 1:<br />

DoSomething();<br />

case 2:<br />

DoMore();<br />

}<br />

Falls nVar bei Eintritt in die switch-Anweisung den Wert 1 hat, werden in C/C++<br />

also beide Zweige ausgeführt. Das sprachliche Potenzial dieser Konzeption ist<br />

zwar mächtig, seine Implizitheit lädt aber auch dazu ein, schwer aufzufindende<br />

Fehler zu produzieren. Bei C# hat man daher darauf verzichtet. Was aber, wenn<br />

man für den einen oder anderen Fall die gleichen Zweige hintereinander ausführen<br />

möchte? Listing 6.3 zeigt, wie das in C# gelöst ist:<br />

Listing 6.3:<br />

Einsatz von goto label und goto default in einer switch-Anweisung<br />

1: using System;<br />

2:<br />

3: class SwitchApp<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: Random objRandom = new Random();<br />

8: double dRndNumber = objRandom.NextDouble();<br />

9: int nRndNumber = (int)(dRndNumber * 10.0);<br />

10:<br />

11: switch (nRndNumber)<br />

12: {<br />

13: case 1:<br />

14: // nichts tun<br />

15: break;<br />

16: case 2:<br />

17: goto case 3;<br />

18: case 3:<br />

19: Console.WriteLine("Fall 2 oder 3");<br />

20: break;<br />

21: case 4:<br />

22: goto default;<br />

23: // Weiterer Code in diesem Zweig ist nicht erreichbar


96<br />

Bedingte Ausführung<br />

24: // und wird vom Compiler mit einer Warnung bedacht.<br />

25: default:<br />

26: Console.WriteLine("Zufallszahl lautet: {0}", nRndNumber);<br />

27: }<br />

28: }<br />

29: }<br />

In diesem Beispiel generiert der in der Klasse Random verkapselte Zufallsgenerator<br />

den Wert des Steuerausdrucks (Zeilen 7 bis 9). Der switch-Block enthält zwei<br />

Sprunganweisungen, die Zweige miteinander verketten:<br />

• goto case AndererFall – Sprung in den Zweig für einen anderen Fall<br />

• goto default – Sprung in den Zweig default<br />

Mit diesen beiden Sprunganweisungen ist das Ausdruckspotenzial der switch-<br />

Kontrollstruktur in C# sogar noch reichhaltiger als in C/C++ und zugleich sinkt<br />

das Fehlerpotenzial, da jeder Sprung explizit formuliert werden muss. Warum<br />

»reichhaltiger»? Nun, erstens spielt die Anordnung der Zweige keine Rolle mehr,<br />

so dass man beispielsweise den default-Zweig an den Anfang setzen kann, um<br />

die Lesbarkeit des Codes zu verbessern; zweitens lassen sich mit C# auch Verkettungen<br />

formulieren, die in C/C++ allein durch die Reihenfolge nicht mehr ausgedrückt<br />

werden können – so etwa, wenn Fall 1 und Fall 2 jeweils mit dem Fall<br />

default verkettet sein sollen, wie das folgende Beispiel zeigt:<br />

switch (nSomething)<br />

{<br />

default:<br />

DoDefault()<br />

case 1:<br />

DoCase1();<br />

goto default;<br />

case 2:<br />

DoCase2();<br />

goto default;<br />

}<br />

Vielleicht ist es Ihnen bereits aufgefallen: In C# kann der switch-Steuerausdruck<br />

auch den Typ string haben. Auch wenn das Visual-Basic-Programmierern nicht<br />

mehr als ein Achselzucken entlocken wird, stellt es gegenüber C/C++ eine wirklich<br />

sensationelle Ausweitung der Ausdruckskraft dar.<br />

Die folgende switch-Anweisung führt eine Fallunterscheidung auf der Basis von<br />

Zeichenfolgen durch:<br />

string strTest = "Chris";<br />

switch (strTest)<br />

{


Kapitel 6 • Kontrollstrukturen 97<br />

}<br />

case "Chris":<br />

Console.WriteLine("Hallo Chris!");<br />

break;<br />

6.2 Schleifen<br />

Wenn Sie in C# eine einzelne Anweisung oder einen Anweisungsblock mehrfach<br />

ausführen wollen, stehen Ihnen vier unterschiedliche Schleifenarten zur Auswahl.<br />

Die entsprechenden Anweisungen sind:<br />

• for<br />

• foreach<br />

• while<br />

• do<br />

6.2.1 for<br />

Die for-Anweisung ist speziell dann nützlich, wenn bereits vor Eintritt in die<br />

Schleife bekannt ist, wie viele Durchläufe es geben soll. Ihre Syntax ist wieder<br />

einmal C/C++ entlehnt, so dass sie von der Ausdruckskraft her den anderen<br />

Schleifen um nichts nachsteht – im Gegenteil, sie bietet sogar die meiste Flexibilität.<br />

Die Schleife wird so lange durchlaufen, wie die (optionale) Abbruchbedingung<br />

erfüllt ist oder ein Abbruch mit break erfolgt. Zudem besteht natürlich die<br />

(gleichfalls optionale) Möglichkeit, Laufvariablen zu initialisieren und nach<br />

jedem Schleifendurchlauf zu aktualisieren. Die allgemeine Form lautet:<br />

for (Initialisierung; Abbruchbedingung; Aktualisierung) Anweisung<br />

Beachten Sie, dass die Bestandteile Initialisierung, Abbruchbedingung und<br />

Aktualisierung jeweils optional sind. Vom Prinzip her können Sie mit der for-<br />

Anweisung auch eine Endlosschleife formulieren, die durch eine bedingte breakoder<br />

goto-Anweisung abgebrochen wird:<br />

for (;;)<br />

{<br />

if(bCondition) break; // bedingter Abbruch der Endlosschleife<br />

}<br />

Ein weiterer wichtiger Punkt ist, dass jeder der drei Bestandteile mehrere durch<br />

Komma getrennte Anweisungen umfassen kann. Sie könnten also beispielsweise<br />

im Initialisierungsteil einer for-Anweisung zwei Variablen initialisieren und im<br />

Aktualisierungsteil vier Variablen aktualisieren.


98<br />

Schleifen<br />

Als C/C++-Programmierer müssen Sie nur auf eines achten: Der Ergebnistyp von<br />

Abbruchbedingung muss bool sein – wie bei der if-Anweisung also.<br />

Listing 6.4 zeigt ein Programm, das die Fakultät eines als Kommandozeilenargument<br />

übergebenen Zahlenwertes über eine typische for-Schleife iterativ berechnet<br />

(was bekanntlich schneller geht als rekursiv):<br />

Listing 6.4:<br />

Fakultät in einer for-Schleife berechnen<br />

1: using System;<br />

2:<br />

3: class Factorial<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: long nFactorial = 1;<br />

8: long nComputeTo = Int64.Parse(args[0]);<br />

9:<br />

10: long nCurDig = 1;<br />

11: for (nCurDig=1; nCurDig


Kapitel 6 • Kontrollstrukturen 99<br />

for ( ; ; )<br />

{<br />

if (nCurDig > nComputeTo) break;<br />

nFactorial *= nCurDig++;<br />

}<br />

Neben der break-Anweisung, die den Abbruch der for-Schleife bewirkt, gibt es<br />

noch die continue-Anweisung. Sie überspringt den Rest des Schleifenkörpers,<br />

verhindert aber nicht den Wiedereintritt.<br />

for (; nCurDig


100<br />

Schleifen<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: IDictionary envvars = Environment.GetEnvironmentVariables();<br />

9: Console.WriteLine("Es gibt {0} Umgebungsvariablen",<br />

envvars.Keys.Count);<br />

10: foreach (string strKey in envvars.Keys)<br />

11: {<br />

12: Console.WriteLine("{0} = {1}", strKey, envvars[strKey].<br />

ToString());<br />

13: }<br />

14: }<br />

15: }<br />

Der Aufruf von GetEnvironmentVariables in Zeile 8 liefert eine Referenz auf die<br />

Schnittstelle IDictionary, für die viele Klassen des NGWS-Subsystems eine Implementation<br />

bereitstellen. IDictionary sieht die Definition zweier Auflistungen<br />

vor: Keys und Values. Eine davon, Keys, durchläuft der Code im Rahmen einer<br />

foreach-Anweisung, um die Namen und die Werte der Umgebungsvariablen der<br />

Reihe nach auszugeben (Zeile 12).<br />

Beim Einsatz von foreach müssen Sie auf eines besonders achten: dass Sie den<br />

richtigen Typ für die Laufvariable deklarieren. Ein falscher Typ wird nämlich<br />

nicht in jedem Fall vom Compiler entdeckt, wohl aber zur Laufzeit, wo er einen<br />

Ausnahmefehler verursacht.<br />

6.2.3 while<br />

Wenn es darum geht, eine Anweisung schlicht in Abhängigkeit von einer Bedingung<br />

wiederholt auszuführen, ist die while-Anweisung das Mittel der Wahl. Die<br />

allgemeine Form dieser Anweisung lautet:<br />

while (Abbruchbedingung) Anweisung<br />

Die Abbruchbedingung, ein Ausdruck, für den C# den Ergebnistyp bool fordert,<br />

wird vor jedem (Wieder-)Eintritt in den Schleifenkörper ausgewertet. Sie<br />

bestimmt damit implizit, wie oft der Schleifenkörper Anweisung durchlaufen<br />

wird. Wie for definiert die while-Anweisung eine abweisende Schleife, deren<br />

Schleifenkörper nicht unbedingt durchlaufen werden muss. Und auch break sowie<br />

continue sind (mit gleicher Wirkung) zulässig.<br />

Um den Gebrauch der while-Anweisung zu demonstrieren, zeigt Listing 6.6 die<br />

Ausgabe einer C#-Quelldatei über die Konsole:<br />

Listing 6.6:<br />

1: using System;<br />

2: using System.IO;<br />

Ausgabe einer Datei über die Konsole


Kapitel 6 • Kontrollstrukturen 101<br />

6.2.4 do<br />

3:<br />

4: class WhileDemoApp<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: StreamReader sr = File.OpenText ("whilesample.cs");<br />

9: String strLine = null;<br />

10:<br />

11: while (null != (strLine = sr.ReadLine()))<br />

12: {<br />

13: Console.WriteLine(strLine);<br />

14: }<br />

15:<br />

16: sr.Close();<br />

17: }<br />

18: }<br />

Der Code öffnet die Datei whilesample.cs im Textmodus. Eine while-Anweisung<br />

liest den Inhalt der Datei Zeile für Zeile und gibt ihn an der Konsole aus, bis die<br />

Methode ReadLine den Wert null anstelle einer Zeichenfolge zurückliefert.<br />

Beachten Sie, dass der Bedingungsteil der Schleife eine Wertzuweisung enthält,<br />

deren Ausführung in einem logischen Ausdruck mit || oder && nur dann gewährleistet<br />

wäre, wenn sie den ersten (linken) Operanden bildet.<br />

Die vierte und letzte Schleifenart von C# liegt in Form der do-Anweisung vor. Als<br />

nicht abweisendes Gegenstück der while-Anweisung wertet die do-Anweisung<br />

die Abbruchbedingung erst nach Durchlaufen des Schleifenkörpers aus. Die allgemeine<br />

Form lautet:<br />

do<br />

{<br />

Anweisung<br />

}<br />

while (Abbruchbedingung);<br />

Der Schleifenkörper Anweisung einer do-Anweisung wird in jedem Fall einmal<br />

durchlaufen. Weitere Durchläufe steuert dann der Wahrheitswert des Booleschen<br />

Ausdrucks Abbruchbedingung. Wie bei den anderen Schleifenarten sind auch bei<br />

der do-Schleife die Anweisungen break und continue (mit gleicher Wirkung)<br />

zulässig.<br />

Listing 6.7 zeigt ein Beispiel für eine do-Anweisung. Das Programm fordert den<br />

Benutzer in einer do-Schleife wiederholt auf, eine Zahl einzugeben. Sobald dieser<br />

die Eingabe einer weiteren Zahl ablehnt, bricht die Schleife ab, und das Programm<br />

gibt das arithmetische Mittel aller eingegebenen Zahlen aus:


102<br />

Schleifen<br />

Listing 6.7:<br />

Arithmetisches Mittel in einer do-Schleife berechnen<br />

1: using System;<br />

2:<br />

3: class ComputeAverageApp<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: ComputeAverageApp theApp = new ComputeAverageApp();<br />

8: theApp.Run();<br />

9: }<br />

10:<br />

11: public void Run()<br />

12: {<br />

13: double dValue = 0;<br />

14: double dSum = 0;<br />

15: int nNoOfValues = 0;<br />

16: char chContinue = 'j';<br />

17: string strInput;<br />

18:<br />

19: do<br />

20: {<br />

21: Console.Write("Bitte eine Zahl eingeben: ");<br />

22: strInput = Console.ReadLine();<br />

23: dValue = Double.Parse(strInput);<br />

24: dSum += dValue;<br />

25: nNoOfValues++;<br />

26: Console.Write("Noch eine Zahl (j/n)? ");<br />

27:<br />

28: strInput = Console.ReadLine();<br />

29: chContinue = Char.FromString(strInput);<br />

30: }<br />

31: while ('j' == chContinue);<br />

32:<br />

33: Console.WriteLine("Arithmetisches Mittel: {0}", dSum /<br />

nNoOfValues);<br />

34: }<br />

35: }<br />

In Abweichung zu den bisherigen Beispielen legt die Methode Main hier ein<br />

Objekt der Klasse ComputeAverageApp an und ruft dann dessen Run-Methode auf,<br />

in der die eigentliche Logik für die Berechnung des arithmetischen Mittels enthalten<br />

ist.<br />

Die do-Anweisung zieht sich über die Zeilen 19 bis 31. Die Abbruchbedingung<br />

prüft, ob der Benutzer die Frage nach Eingabe einer weitere Zahl mit »j« beantwortet<br />

hat. Jedes andere Zeichen führt zum Abbruch der Schleife und zur Ausgabe<br />

des arithmetischen Mittels.


Kapitel 6 • Kontrollstrukturen 103<br />

Das Beispiel macht den einzigen Unterschied zwischen do und while deutlich:<br />

Die while-Anweisung prüft die Abbruchbedingung bereits vor dem ersten Durchlaufen<br />

des Schleifenkörpers, die do-Anweisung erst danach.<br />

6.3 Zusammenfassung<br />

Dieses Kapitel hat Ihnen die in C# verfügbaren Kontrollstrukturen für die<br />

bedingte und die wiederholte Ausführung von Anweisungen vorgestellt. Am häufigsten<br />

werden Sie in Ihren Programmen wahrscheinlich die if-Anweisung einsetzen.<br />

Da Sie der Compiler dazu zwingt, Bedingungen im Datentyp bool zu formulieren,<br />

ist eine Verwechslung von = und == ausgeschlossen. Sie müssen nur<br />

darauf aufpassen, dass die Operanden Boolescher Ausdrücke keine wertverändernden<br />

Berechnungen anstellen, da in C# die Auswertung des zweiten Operanden<br />

in Booleschen Ausdrücken mit && und || nicht garantiert ist.<br />

Die aus der C-Welt stammende switch-Anweisung hat sich in C# ebenfalls zu<br />

ihrem Vorteil verändert. Sie müssen es nun explizit formulieren, wenn für einen<br />

Fall mehrere Zweige durchlaufen werden sollen. Die Reihenfolge der Zweige<br />

spielt keine Rolle mehr, und die Ausdruckskraft hat sich verbessert. Darüber<br />

hinaus lässt sich die Fallunterscheidung in C# auch mit dem Datentyp string formulieren<br />

– was in C/C++ nicht möglich ist.<br />

In der zweiten Hälfte des Kapitels ging es um die vier Schleifenanweisungen for,<br />

foreach, while und do. Jede der Anweisungen dient einem anderen Zweck: for<br />

ermöglicht eine feste Anzahl von Schleifendurchläufen; foreach zählt die Elemente<br />

von Auflistungen auf; while implementiert eine abweisende Schleife mit<br />

implizitem Kriterium; do kontrolliert eine nicht abweisende Schleife mit implizitem<br />

Kriterium.


Kapitel 7<br />

Ausnahmen behandeln<br />

7.1 Die Anweisungen checked und unchecked 106<br />

7.2 Anweisungen für die Ausnahmebehandlung 108<br />

7.3 Ausnahmen signalisieren 115<br />

7.4 Ausnahmen richtig dosiert 118<br />

7.5 Zusammenfassung 119


106<br />

Die Anweisungen checked und unchecked<br />

Ein großer Vorteil des NGWS-Laufzeitsystems ist, dass es die Ausnahmebehandlung<br />

für verschiedene Sprachen standardisiert. Auf diese Weise kann eine von C#<br />

signalisierte Ausnahme jederzeit in einem Visual-Basic-Client behandelt werden<br />

oder umgekehrt. HRESULT-Werte und Schnittstellen à la ISupportErrorInfo werden<br />

in absehbarer Zeit wohl ausgedient haben.<br />

Obwohl die sprachübergreifende Ausnahmebehandlung eine großartige Sache ist,<br />

konzentriert sich dieses Kapitel ausschließlich auf die Ausnahmebehandlung<br />

innerhalb von C#. Man dreht ein wenig am Verhalten des Compilers, was die<br />

Behandlung von Überläufen angeht, und schon geht der Spaß los: Überläufe führen<br />

zu Ausnahmen, und Ausnahmen lassen sich im Code auffangen und behandeln.<br />

Das ist aber nur die eine Seite der Medaille; die andere sieht so aus, dass<br />

man Ausnahmen selbst signalisiert und sogar implementiert.<br />

7.1 Die Anweisungen checked und unchecked<br />

Im Verlauf einer Berechnung kann es schnell einmal passieren, dass das errechnete<br />

Ergebnis nicht mehr im Wertebereich des zugrunde gelegten Datentyps liegt.<br />

Diese Situation wird gemeinhin als Überlauf bezeichnet, und je nach verwendeter<br />

Programmiersprache erhält der Benutzer davon in irgendeiner Form Kenntnis –<br />

oder eben auch nicht. (Kommt Ihnen dies von C++ her etwa bekannt vor?)<br />

Wie also verfährt C# mit Überläufen? Um das diesbezügliche Standardverhalten<br />

der Sprache zu erkunden, betrachten Sie noch einmal das bereits in Kapitel 6 vorgestellte<br />

Programm, das die Fakultät einer Zahl berechnet. Zu Ihrer Bequemlichkeit<br />

ist es hier noch einmal abgedruckt:<br />

Listing 7.1:<br />

Programm zur Berechnung der Fakultät einer Zahl<br />

1: using System;<br />

2:<br />

3: class Factorial<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: long nFactorial = 1;<br />

8: long nComputeTo = Int64.Parse(args[0]);<br />

9:<br />

10: long nCurDig = 1;<br />

11: for (nCurDig=1; nCurDig


Kapitel 7 • Ausnahmen behandeln 107<br />

Wenn Sie dem Programm beim Aufruf nun ein etwas zu »großzügig« bemessenes<br />

Argument mitgeben, etwa:<br />

factorial 2000<br />

gibt es als Ergebnis den Wert 0 aus – ohne weiteren Kommentar. Mit anderen<br />

Worten, Sie können davon ausgehen, dass C# Überlaufsituationen mit Diskretion<br />

behandelt und keine expliziten Warnungen von sich gibt.<br />

Dieses Verhalten lässt sich ändern, indem Sie einen Compilerschalter setzen, der<br />

die Überlaufkontrolle für das gesamte Programm aktiviert, oder indem Sie die<br />

Überlaufkontrolle auf der Ebene einzelner Anweisungen aktivieren. Die zwei folgenden<br />

Abschnitte diskutieren diese beiden Ansätze.<br />

7.1.1 Überlaufkontrolle per Compiler-Schalter aktivieren<br />

Sofern Sie für die gesamte Anwendung eine Überlaufkontrolle wünschen, reicht<br />

es, den Compilerschalter /checked+ zu setzen, der standardmäßig deaktiviert ist.<br />

Der Aufruf für die Übersetzung des Beispielprogramms lautet dann:<br />

csc factorial.cs /checked+<br />

Wenn Sie das auf diese Weise kompilierte Programm nun mit dem Kommandozeilenargument<br />

2000 aufrufen, meldet Ihnen das NGWS-Laufzeitsystem einen<br />

unbehandelten Laufzeitfehler (vgl. Abbildung 7.1).<br />

Bild 7.1:<br />

Bei aktivierter Überlaufkontrolle scheitert die Berechnung der Fakultät für<br />

die Zahl 2000<br />

Nach Bestätigung des Dialogfelds mit OK erhalten Sie über die Konsole zusätzlich<br />

die Fehlermeldung:<br />

Exception occurred: System.OverflowException<br />

at Factorial.Main(System.String[])<br />

Dem lässt sich entnehmen, dass ein Überlauf die Ausnahme System.Overflow-<br />

Exception auslöst. Über die Behandlung solcher Ausnahmen gleich mehr im<br />

Anschluss an den folgenden Abschnitt.


108<br />

Anweisungen für die Ausnahmebehandlung<br />

7.1.2 Überlaufkontrolle vom Code aus steuern<br />

Falls Ihnen eine Überlaufkontrolle für den gesamten Code zuviel des Guten ist,<br />

werden Sie es wahrscheinlich begrüßen, dass C# in Form der Anweisung checked<br />

die Aktivierung der Überlaufkontrolle auch auf der Ebene einzelner Anweisungen<br />

bzw. Codeblocks unterstützt. Listing 7.2 zeigt, wie das geht:<br />

Listing 7.2:<br />

Berechnung der Fakultät mit Überlaufkontrolle für die gefährdete<br />

Anweisung<br />

1: using System;<br />

2:<br />

3: class Factorial<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: long nFactorial = 1;<br />

8: long nComputeTo = Int64.Parse(args[0]);<br />

9:<br />

10: long nCurDig = 1;<br />

11: for (nCurDig=1; nCurDig


Kapitel 7 • Ausnahmen behandeln 109<br />

mit C++ programmiert haben, wird Ihnen die strukturierte Ausnahmebehandlung<br />

(SEH = structured exception handling) sicher geläufig sein. In C# sieht die Sache<br />

vom Prinzip her ähnlich aus.<br />

Die folgenden drei Abschnitte stellen Ihnen die C#-Anweisungen für die strukturierte<br />

Ausnahmebehandlung vor:<br />

• Ausnahmen mit try...catch behandeln<br />

• Aufräumcode mit try...finally ausführen<br />

• Ausnahmen mit try...catch...finally voll im Griff<br />

7.2.1 Ausnahmen mit try...catch behandeln<br />

Wenn Ausnahmen ins Spiel kommen, sollten Sie sich darum kümmern, dem Benutzer<br />

entsprechende Meldungen des Laufzeitsystems zu ersparen und den Abbruch<br />

des Programms respektive die Aktivierung des Debuggers zu vermeiden. Mit anderen<br />

Worten: Es ist erforderlich, die Ausnahmen abzufangen und in irgendeiner<br />

Form sinnvoll zu behandeln, damit die Ausführung fortgesetzt werden kann.<br />

Die Anweisungen, die Ihnen dies ermöglichen, sind try und catch. Ein try-Block<br />

enthält die (regulären) Anweisungen des Programms, die möglicherweise zu einer<br />

Ausnahme führen; ein catch-Block enthält zusätzliche Anweisungen für die Fehlerbehandlung<br />

im Falle einer Ausnahme. Listing 7.3 zeigt eine Implementation<br />

des diskutierten Programms, die die Ausnahme OverflowException mittels try<br />

und catch behandelt:<br />

Listing 7.3:<br />

Behandlung der Ausnahme OverflowException<br />

1: using System;<br />

2:<br />

3: class Factorial<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: long nFactorial = 1, nCurDig=1;<br />

8: long nComputeTo = Int64.Parse(args[0]);<br />

9:<br />

10: try<br />

11: {<br />

12: checked<br />

13: {<br />

14: for (; nCurDig


110<br />

Anweisungen für die Ausnahmebehandlung<br />

20: Console.WriteLine("Überlauf bei der Berechnung von {0}!",<br />

nComputeTo);<br />

21: return;<br />

22: }<br />

23:<br />

24: Console.WriteLine("{0}! ist {1}", nComputeTo, nFactorial);<br />

25: }<br />

26: }<br />

Der Klarheit halber wurden die einzelnen Blocks hier durch geschweifte Klammern<br />

markiert, auch wenn dies nicht in jedem Fall erforderlich war. Der Code<br />

enthält an der bewussten Stelle eine checked-Anweisung, so dass die Fehlerbehandlung<br />

auch dann zum Zuge kommen kann, wenn der angesprochene Compilerschalter<br />

nicht gesetzt wurde.<br />

Wie Sie sehen, ist die Fehlerbehandlung selbst nichts Besonderes. Sie müssen<br />

eigentlich nur den fehleranfälligen Code in einen try-Block packen und die zu<br />

erwartende Ausnahme anschließend in einem catch-Block geeignet deklarieren<br />

und einer sorgfältigen Behandlung unterziehen. Die return-Anweisung erwirkt<br />

den Abbruch der Methode.<br />

Falls Ihnen die Art der zu erwartenden Ausnahme nicht bekannt ist, können Sie,<br />

um auf der sicheren Seite zu sein, die Deklaration des Ausnahmeobjekts im<br />

catch-Block auch weglassen:<br />

try<br />

{<br />

...<br />

}<br />

catch<br />

{<br />

...<br />

}<br />

Dieser Ansatz hat natürlich den Nachteil, dass im catch-Block keinerlei Informationen<br />

über die Art des aufgetretenen Fehlers verfügbar sind. Besser ist es, ein allgemeines<br />

Fehlerobjekt mit dem Typ System.Exception, der Basisklasse aller Ausnahmen<br />

für Laufzeitfehler, zu deklarieren. Die allgemeine Form für die<br />

Fehlerbehandlung mit try und catch sieht damit so aus:<br />

try<br />

{<br />

...<br />

}<br />

catch(System.Exception e)<br />

{<br />

...<br />

}


Kapitel 7 • Ausnahmen behandeln 111<br />

Beachten Sie, dass Sie das Objekt e weder in einem ref- oder out-Parameter<br />

übergeben noch als Linkswert in einer Zuweisung verwenden dürfen.<br />

7.2.2 Aufräumcode mit try…finally ausführen<br />

Wenn Ihnen mehr daran gelegen ist, nach einer Ausnahme aufzuräumen, als den<br />

Fehler selbst zu behandeln, sind Sie mit dem try…finally-Konstrukt am besten<br />

bedient. Es unterdrückt zwar die Fehlermeldung des Systems bei Auftreten einer<br />

Ausnahme nicht, gibt Ihnen aber die Möglichkeit, Code zu platzieren, der auf<br />

jeden Fall im Anschluss an den try-Block zur Ausführung kommt, gleich ob ein<br />

Abbruch der Routine erfolgt oder nicht.<br />

Damit kann das Programm beispielsweise dem Benutzer noch eine Meldung ausgeben,<br />

bevor es vom Laufzeitsystem abgebrochen wird. Listing 7.4 zeigt diesen<br />

Fall:<br />

Listing 7.4:<br />

Der Code in einer finally Anweisung wird auf jeden Fall ausgeführt<br />

1: using System;<br />

2:<br />

3: class Factorial<br />

4: {<br />

5: public static void Main(string[] args)<br />

6: {<br />

7: long nFactorial = 1, nCurDig=1;<br />

8: long nComputeTo = Int64.Parse(args[0]);<br />

9: bool bAllFine = false;<br />

10:<br />

11: try<br />

12: {<br />

13: checked<br />

14: {<br />

15: for (; nCurDig


112<br />

Anweisungen für die Ausnahmebehandlung<br />

Nachdem der finally-Block sowohl im regulären als auch im Ausnahmefall zur<br />

Ausführung kommt, muss seine Logik so gestaltet sein, dass er auf beide Fälle<br />

entsprechend reagiert. Das Programm pflegt für diesen Zweck die Zustandsvariable<br />

bAllFine, die nur dann den Wert true annimmt, wenn der Code im try-Block<br />

keine Ausnahme produziert.<br />

Falls Ihnen die strukturierte Ausnahmebehandlung von C++ geläufig ist, werden<br />

Sie sich vielleicht fragen, wie in C# das Gegenstück der __leave-Anweisung aussieht.<br />

(Wenn Sie diese Anweisung noch nicht kennen: __leave wird in C++ dafür<br />

eingesetzt, einen try-Block vorzeitig zu verlassen und unmittelbar in den<br />

finally-Block zu verzweigen.) Leider gibt es ein solches Gegenstück in C# nicht,<br />

so dass Sie darauf angewiesen sind, sich anders zu behelfen. Listing 7.5 zeigt wie:<br />

Listing 7.5:<br />

Vom try-Block aus in den finally-Block verzweigen<br />

1: using System;<br />

2:<br />

3: class JumpTest<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: try<br />

8: {<br />

9: Console.WriteLine("try");<br />

10: goto __leave;<br />

11: }<br />

12: finally<br />

13: {<br />

14: Console.WriteLine("finally");<br />

15: }<br />

16:<br />

17: __leave:<br />

18: Console.WriteLine("__leave");<br />

19: }<br />

20: }<br />

Das Programm generiert die folgende Ausgabe:<br />

try<br />

finally<br />

__leave<br />

Wie Sie sehen, muss sich auch die goto-Anweisung der Regel beugen, dass der<br />

finally-Block bei Verlassen des try-Blocks zur Ausführung kommt. Der Sprung<br />

zur Marke __leave erfolgt somit erst nach Abarbeitung des finally-Blocks. Und<br />

da die Sprungmarke unmittelbar auf den finally-Block folgt, geht die Kontrolle<br />

an die Anweisung über, die auch ohne Sprung als Nächstes zur Ausführung


Kapitel 7 • Ausnahmen behandeln 113<br />

gekommen wäre. Der Sprung hat demnach denselben Effekt wie die __leave-<br />

Anweisung des SEH: Das unmittelbare Verlassen des try-Blocks und die Ausführung<br />

des finally-Blocks.<br />

Ein Skeptiker könnte hier natürlich den Verdacht hegen, dass der C#-Compiler die<br />

goto-Anweisung komplett unter den Tisch kehrt, weil sie die letzte Anweisung im<br />

try-Block war und die Kontrolle an dieser Stelle naturgemäß an den finally-<br />

Block übergeht. Um dies zu klären, setzen Sie die goto-Anweisung einfach vor<br />

den Aufruf der Methode Console.WriteLine. Sie erhalten dann zwar eine Compilerwarnung,<br />

dass der Methodenaufruf nicht mehr erreichbar sei, die Ausgabe des<br />

Programms zeigt aber, dass der Sprung stattfindet – die Zeile "try" fehlt nämlich.<br />

7.2.3 Ausnahmen mit try...catch...finally voll im Griff<br />

In der Praxis werden Sie wahrscheinlich beide Techniken für den Umgang mit<br />

Ausnahmen einsetzen: Sie fangen die Ausnahmen erst ab, um sie zu behandeln<br />

und räumen dann auf, damit die reguläre Ausführung des Programms weitergehen<br />

kann. Mit anderen Worten: Ihr Fehlerbehandlungscode arbeitet mit allen drei<br />

Anweisungen: try, catch und finally. Listing 7.6 zeigt, wie der Umgang mit<br />

Fehlern aussehen kann, die bei Divisionen durch Null ausgelöst werden:<br />

Listing 7.6:<br />

Mehrere catch-Blöcke<br />

1: using System;<br />

2:<br />

3: class CatchIT<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: try<br />

8: {<br />

9: int nTheZero = 0;<br />

10: int nResult = 10 / nTheZero;<br />

11: }<br />

12: catch(DivideByZeroException divEx)<br />

13: {<br />

14: Console.WriteLine("Division durch Null!");<br />

15: }<br />

16: catch(Exception Ex)<br />

17: {<br />

18: Console.WriteLine("Andere Ausnahme");<br />

19: }<br />

20: finally<br />

21: {<br />

22: }<br />

23: }<br />

24: }


114<br />

Anweisungen für die Ausnahmebehandlung<br />

Das Besondere an diesem Beispiel ist, dass der Code gleich zwei catch-Blöcke<br />

für denselben try-Block verwendet. Der erste catch-Block kümmert sich speziell<br />

um die häufiger auftretende Ausnahme DivideByZeroException und der zweite<br />

um alle restlichen Ausnahmen.<br />

Es ist wichtig, dass Sie catch-Blöcke so anreihen, dass der Code für die Behandlung<br />

spezieller Ausnahmen vor dem für allgemeinere Ausnahmen zu stehen<br />

kommt. Das Beispiel in Listing 7.7 zeigt, was passiert, wenn Sie sich nicht an<br />

diese Reihenfolge halten:<br />

Listing 7.7:<br />

catch-Blöcke in falscher Reihenfolge<br />

1: try<br />

2: {<br />

3: int nTheZero = 0;<br />

4: int nResult = 10 / nTheZero;<br />

5: }<br />

6: catch(Exception Ex)<br />

7: {<br />

8: Console.WriteLine("Allgemeine Ausnahme" + Ex.ToString());<br />

9: }<br />

10: catch(DivideByZeroException divEx)<br />

11: {<br />

12: Console.WriteLine("Und sie ward nie gesehen...");<br />

13: }<br />

Der Compiler bemerkt den Verdreher und generiert eine Fehlermeldung nach folgendem<br />

Muster:<br />

wrongcatch.cs(10,9): error CS0160: A previous catch clause already catches<br />

all exceptions of this or a super type ('System.Exception')<br />

Zum Schluss dieser Betrachtung sollte nicht unerwähnt bleiben, das der Ausnahmemechanismus<br />

des NGWS-Laufzeitsystems gegenüber dem SEH folgenden<br />

Mangel (oder wenn Sie so wollen: Unterschied) aufweist: Es gibt kein Äquivalent<br />

für den in SEH-Ausnahmefiltern definierten Bezeichner EXCEPTION_CONTINUE_<br />

EXECUTION. Dieser Bezeichner ermöglicht im Wesentlichen die Wiederholung des<br />

für die Ausnahme verantwortlichen Codes, nachdem die Ursache des Fehlers<br />

behoben ist. Eine bevorzugte Speicherverwaltungstechnik des Autors war in diesem<br />

Zusammenhang beispielsweise die Speicheranforderung in Reaktion auf<br />

Ausnahmen aufgrund von Zugriffsverletzungen.


Kapitel 7 • Ausnahmen behandeln 115<br />

7.3 Ausnahmen signalisieren<br />

Ausnahmen kommen nicht von irgendwoher: Sie werden konkret ausgelöst, wenn<br />

es etwas zu signalisieren gibt. Ihr eigener Code ist davon natürlich nicht ausgenommen.<br />

Sie können jederzeit selbst Ausnahmen auslösen, um dem Aufrufer<br />

einen Fehler anzuzeigen. Das Vorgehen ist einfach:<br />

throw new ArgumentException("Argument darf nicht 5 sein");<br />

Sie übergeben einer throw-Anweisung die Instanz einer Ausnahmeklasse. Die im<br />

Beispiel verwendete Ausnahmeklasse ArgumentException gehört zu den Standardausnahmen<br />

des NGWS-Laufzeitsystems, über die Tabelle 7.1 einen Überblick<br />

gibt.<br />

Datentyp der Ausnahme<br />

Exception<br />

SystemException<br />

IndexOutOfRangeException<br />

NullReferenceException<br />

InvalidOperationException<br />

ArgumentException<br />

ArgumentNullException<br />

ArgumentOutOfRangeException<br />

InteropException<br />

ComException<br />

SEHException<br />

Tab. 7.1:<br />

Beschreibung<br />

Basisklasse für alle Ausnahmen<br />

Basisklasse für alle zur Laufzeit generierten Ausnahmen<br />

signalisiert eine Bereichsverletzung für einen<br />

Arrayindex<br />

signalisiert den Zugriff auf eine null-Referenz<br />

wird von verschiedenen Methoden signalisiert,<br />

wenn die Operation für den aktuellen Objektzustand<br />

nicht durchgeführt werden kann<br />

Basisklasse für Ausnahmen, die auf Argumentfehler<br />

zurückgehen<br />

wird von Methoden signalisiert, wenn für einen<br />

Parameter unerwartet das Argument null vorliegt<br />

wird von Methoden signalisiert, wenn für das<br />

Argument eines Parameters eine Bereichsverletzung<br />

vorliegt<br />

Basisklasse für Ausnahmen, die auf Umgebungen<br />

außerhalb des NGWS-Laufzeitsystems bezogen<br />

sind oder daraus stammen<br />

enthält HRESULT-Information im klassischen Stil<br />

des COM<br />

verkapselt Informationen im Stile der strukturierten<br />

Ausnahmebehandlung (SEH) des Win32<br />

Standardausnahmen des NGWS-Laufzeitsystems


116<br />

Ausnahmen signalisieren<br />

Es ist nicht erforderlich, für jeden throw-Aufruf ein neues Ausnahmeobjekt anzulegen.<br />

Sofern in einem catch-Block bereits ein entsprechendes Objekt zur Hand<br />

ist, können Sie dieses jederzeit weiterverwenden. Andererseits gibt es auch Situationen,<br />

in denen keiner der in Tabelle 7.1 genannten standardmäßigen Ausnahmetypen<br />

Ihren speziellen Bedürfnissen gerecht wird. Sie werden dann nicht<br />

umhinkommen, selbst eine passende Ausnahmeklasse abzuleiten und zu implementieren.<br />

Die folgenden zwei Abschnitte vertiefen diese beiden Aspekte.<br />

7.3.1 Ausnahmen weiterleiten<br />

Bei der Implementation eines catch-Blocks stehen Sie vor der Wahl, ob Sie eine<br />

abgefangene Ausnahme vor Ort behandeln oder schlicht an den Aufrufer weiterleiten,<br />

um die Behandlung einem weiter außen gelegenen try…catch-Block zu<br />

überlassen. Listing 7.8 zeigt ein Beispiel für diesen Ansatz:<br />

Listing 7.8:<br />

Eine Ausnahme an den Aufrufer weiterleiten<br />

1: try<br />

2: {<br />

3: checked<br />

4: {<br />

5: for (; nCurDig


Kapitel 7 • Ausnahmen behandeln 117<br />

Listing 7.9 stellt die Implementation einer eigenen Ausnahmeklasse namens<br />

MyImportantException vor. Sie hält sich an zwei Regeln: Erstens trägt der Klassenbezeichner<br />

das Suffix Exception und zweitens definiert die Klasse alle drei für<br />

Ausnahmeklassen empfohlenen Konstruktoren. An diese Regeln sollten auch Sie<br />

sich halten:<br />

Listing 7.9:<br />

Implementation der eigenen Ausnahmeklasse MyImportantException<br />

1: using System;<br />

2:<br />

3: public class MyImportantException:Exception<br />

4: {<br />

5: public MyImportantException()<br />

6: :base() {}<br />

7:<br />

8: public MyImportantException(string message)<br />

9: :base(message) {}<br />

10:<br />

11: public MyImportantException(string message, Exception inner)<br />

12: :base(message,inner) {}<br />

13: }<br />

14:<br />

15: public class ExceptionTestApp<br />

16: {<br />

17: public static void TestThrow()<br />

18: {<br />

19: throw new MyImportantException("Etwas Schreckliches ist<br />

passiert");<br />

20: }<br />

21:<br />

22: public static void Main()<br />

23: {<br />

24: try<br />

25: {<br />

26: ExceptionTestApp.TestThrow();<br />

27: }<br />

28: catch (Exception e)<br />

29: {<br />

30: Console.WriteLine(e);<br />

31: }<br />

32: }<br />

33: }<br />

Wie Sie sehen, enthält die Implementation von MyImportantException keinerlei<br />

Spezialitäten, sondern verlässt sich vollständig auf die Basisklasse System.Exception.<br />

Der restliche Code des Programms testet die Ausnahmeklasse mittels eines<br />

catch-Blocks, der auf Ausnahmen des Typs System.Exception zugeschnitten ist.


118<br />

Ausnahmen richtig dosiert<br />

Nachdem sich die Implementation der Klasse eigentlich nur auf die standardmäßige<br />

Ausnahmeklasse System.Exception stützt – die drei Konstruktoren rufen<br />

schlicht die jeweilige Basisklassenvariante auf – stellt sich natürlich die Frage,<br />

was die Ableitung einer eigenen Ausnahmeklasse überhaupt für einen Sinn hat.<br />

Das Besondere daran ist natürlich der neue Typ. Er kann in einem catch-Block als<br />

Auswahlkriterium fungieren und erlaubt es, Code speziell auf diesen Ausnahmetyp<br />

zuzuschneiden.<br />

Wenn Sie eine Klassenbibliothek mit eigenem Namensraum implementieren,<br />

sollten Sie die dazugehörigen Ausnahmeklassen gleichfalls in diesen Namensraum<br />

packen. Darüber hinaus bietet es natürlich Vorteile, wenn Ihre Ausnahmeklassen<br />

Eigenschaften für erweiterte Fehlerinformationen definieren – auch wenn<br />

das vorangegangene Beispiel darauf verzichtet hat.<br />

7.4 Ausnahmen richtig dosiert<br />

Abschließend noch eine Liste mit Ratschlägen für den wohldosierten Umgang<br />

mit Ausnahmen:<br />

• Geben Sie jeder Ausnahme, die Sie signalisieren, einen aussagekräftigen Fehlertext<br />

mit.<br />

• Signalisieren Sie nur dann eine Ausnahme, wenn es die Situation wirklich erfordert,<br />

das heißt, wenn ein Funktionswert oder Ausgabeparameter für die gleiche<br />

Aufgabe nicht in Frage kommt.<br />

• Signalisieren Sie eine Ausnahme des Typs ArgumentException, wenn für einen<br />

Aufrufparameter ein fehlerhafter Wert übergeben wurde.<br />

• Signalisieren Sie eine Ausnahme des Typs InvalidOperationException, wenn<br />

die gewünschte Operation aufgrund des aktuellen Objektzustands nicht durchführbar<br />

ist.<br />

• Signalisieren Sie immer den Ausnahmetyp, der für die Situation am besten<br />

passt.<br />

• Arbeiten Sie mit verketteten Ausnahmen – dadurch wird es möglich, den Weg<br />

einer Ausnahme zurückzuverfolgen.<br />

• Unterlassen Sie es, Ausnahmen für gewöhnliche Situationen oder reguläre<br />

Fehler zu signalisieren.<br />

• Unterlassen Sie es, Ausnahmen im Rahmen der regulären Programmlogik einzusetzen<br />

• Unterlassen Sie es, Ausnahmen des Typs NullReferenceException oder IndexOutOfRangeException<br />

innerhalb von Methoden zu signalisieren.


Kapitel 7 • Ausnahmen behandeln 119<br />

7.5 Zusammenfassung<br />

Ausgangspunkt für dieses Kapitel war der Umgang mit Überläufen. Ein Compilerschalter<br />

erlaubt es Ihnen, die in C# standardmäßig unterdrückte Signalisierung<br />

von Überläufen für die gesamte Anwendung einzuschalten. Falls Sie eine differenzierte<br />

Kontrolle wünschen, stehen Ihnen die Anweisungen checked und unchecked<br />

zur Verfügung, die die Überlaufkontrolle blockorientiert und unabhängig<br />

von den Compilereinstellungen ein- bzw. ausschalten.<br />

Erkennt das Laufzeitsystem einen Überlauf, signalisiert es diesen als Ausnahme.<br />

Das Kapitel hat drei Ansätze diskutiert, wie Ihr Code auf eine Ausnahme reagieren<br />

kann. Der allgemeinste Ansatz beinhaltet die Implementation von try-, catchund<br />

finally-Blöcken. Anhand verschiedener Beispiele haben Sie darüber hinaus<br />

erfahren, welche Unterschiede der Ausnahmemechanismus von C# gegenüber der<br />

strukturierten Ausnahmebehandlung (SEH) des Win32 aufweist.<br />

Die Behandlung von Ausnahmen wird erforderlich, wenn Sie mit fertigen Klassen<br />

arbeiten, die Signalisierung dagegen, wenn Sie Ihre eigenen Klassen implementieren.<br />

Für die Signalisierung sind drei Situationen zu unterscheiden: Es kann<br />

erstens notwendig sein, nicht behandelbare Ausnahmen als solche weiterzuleiten,<br />

zweitens, erkannte Fehler in Form standardmäßiger Ausnahmen des NGWS-<br />

Laufzeitsystems zu signalisieren, und drittens, eigene Ausnahmeklassen zu implementieren,<br />

um Fehler in spezifischen Zusammenhängen hinreichend präzisieren<br />

zu können.<br />

Den Ausklang des Kapitels bildeten schließlich noch verschiedene Ratschläge für<br />

den wohldosierten und bedachten Umgang mit Ausnahmen.


Kapitel 8<br />

Komponenten mit C# schreiben<br />

8.1 Die erste Komponente 122<br />

8.2 Namensräume 127<br />

8.3 Zusammenfassung 134


122<br />

Die erste Komponente<br />

In diesem Kapitel geht es um das Schreiben von Komponenten in C#: Wie entsprechende<br />

Quelltexte aussehen müssen, wie man sie kompiliert, und wie sich das<br />

Ergebnis in einer Client-Anwendung verwenden lässt. Die im zweiten Teil des<br />

Kapitels behandelten Namensräume helfen bei der Organisation von Anwendungen.<br />

Die folgenden Seiten sind also in zwei große Abschnitte unterteilt:<br />

• die erste Komponente<br />

• Namensräume<br />

8.1 Die erste Komponente<br />

Die bis dato vorgestellten Beispiele haben sämtlich Klassen in ein und derselben<br />

Anwendung sowohl deklariert als auch eingesetzt – was nicht unbedingt so sein<br />

muss: Wenn Sie eine Klasse als Komponente formulieren und in einer separaten<br />

Datei unterbringen, dann lässt sich diese Klasse auch von mehreren Anwendungen<br />

aus nutzen.<br />

In C# geschriebene Komponenten werden in DLLs untergebracht, unterscheiden<br />

sich aber stark von den üblicherweise in C++ geschriebenen COM-Komponenten;<br />

unter anderem bekommen Sie es bei C# mit wesentlich weniger Details der von<br />

COM her gewohnten Infrastruktur zu tun. Die folgenden Abschnitte zeigen, wie<br />

man in C# eine Komponente erstellt, kompiliert und schließlich in einer Anwendung<br />

einsetzt:<br />

• Erstellen einer Komponente<br />

• Kompilieren von Komponenten<br />

• Erstellen einer einfachen Client-Anwendung<br />

8.1.1 Erstellen einer Komponente<br />

Obwohl es primär um eine Demonstration geht, versteht sich das folgende Beispiel<br />

nicht als Selbstzweck: Es geht um eine Klasse, die eine Webseite von einem<br />

Server lädt und den HTML-Code als String zur weiteren Verwendung zur Verfügung<br />

stellt. Allzu kompliziert ist diese Klasse nicht, weil sie den größten Teil der<br />

Arbeit dem NWGS-Subsystem überlässt.<br />

Die Klasse RequestWebPage hat zwei Konstruktoren, eine Eigenschaft und eine<br />

Methode. Die Eigenschaft trägt den Namen URL und speichert die Adresse der<br />

Webseite, deren Inhalt sich über die Methode GetContent laden lässt.


Kapitel 8 • Komponenten mit C# schreiben 123<br />

Listing 8.1:<br />

Die Klasse RequestWebPage für das Herunterladen von Webseiten<br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: public class RequestWebPage<br />

7: {<br />

8: private const int BUFFER_SIZE = 128;<br />

9: private string m_strURL;<br />

10:<br />

11: public RequestWebPage()<br />

12: {<br />

13: }<br />

14:<br />

15: public RequestWebPage(string strURL)<br />

16: {<br />

17: m_strURL = strURL;<br />

18: }<br />

19:<br />

20: public string URL<br />

21: {<br />

22: get { return m_strURL; }<br />

23: set { m_strURL = value; }<br />

24: }<br />

25: public void GetContent(out string strContent)<br />

26: {<br />

27: // ist ein URL angegeben?<br />

28: if (m_strURL == ""<br />

29: throw new ArgumentException("URL muss gesetzt sein!");<br />

30:<br />

31: WebRequest theRequest =<br />

(WebRequest) WebRequestFactory.Create(m_strURL);<br />

32: WebResponse theResponse = theRequest.GetResponse();<br />

33:<br />

34: // Bytepuffer für die Antwort anlegen<br />

35: int BytesRead = 0;<br />

36: Byte[] Buffer = new Byte[BUFFER_SIZE];<br />

37:<br />

38: Stream ResponseStream = theResponse.GetResponseStream();<br />

39: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE);<br />

40:<br />

41: // StringBuilder tut sich mit schrittweise vergrößerten Puffern<br />

leichter<br />

42: StringBuilder strResponse = new StringBuilder("");<br />

43: while (BytesRead != 0 )<br />

44: {


124<br />

Die erste Komponente<br />

45: strResponse.Append(Encoding.ASCII.GetString(Buffer, 0,<br />

BytesRead));<br />

46: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE);<br />

47: }<br />

48:<br />

49: // Ausgangsparameter mit dem Ergebnis besetzen<br />

50: strContent = strResponse.ToString();<br />

51: }<br />

52: }<br />

Mit einem einzigen, parameterlosen Konstruktor käme die Klasse natürlich auch<br />

aus. In der Praxis wird es aber meist so sein, dass man beim Anlegen eines<br />

Objekts die zu lesende Webadresse bereits kennt und auch angeben wird. Spätere<br />

Änderungen der Webadresse – beispielsweise zum Lesen einer weiteren Webseite<br />

– sind ohne weiteres über die get- und set-Zugriffsfunktionen für die Eigenschaft<br />

URL möglich.<br />

Interessant wird die Sache in der Methode GetContent. Sie prüft erst einmal auf<br />

recht primitive Weise den Wert von URL – falls dort eine leere Zeichenfolge steht,<br />

löst sie eine ArgumentException aus – und fordert dann von der Klasse WebRequestFactory<br />

das Anlegen eines WebRequest-Objekts für den Wert von URL an.<br />

Objekte dieser Klasse bieten eine Vielzahl von Methoden für das Senden von<br />

Cookies, zusätzlichen Kopfinformationen, Abfragestrings usw., die hier sämtlich<br />

ungenutzt bleiben: GetContent fordert in Zeile 32 ohne weitere Umschweife<br />

direkt eine Antwort in Form eines WebResponse-Objekts an. (Wenn Sie die<br />

Abfrage der Webseite in irgendeiner Weise modifizieren wollen – eben beispielsweise<br />

durch Cookies, – dann müssen Sie die entsprechenden Eigenschaften und<br />

Methoden von WebRequest vor dem Anfordern der Antwort einsetzen.)<br />

Die Anweisungen in den Zeilen 35 und 36 initialisieren einen Bytepuffer, der<br />

dann zum Lesen der Webseite über den mit GetResponseStream angelegten Stream<br />

verwendet wird. Die while-Schleife endet erst, wenn eine Leseoperation 0 Bytes<br />

ergibt, also das Ende des Streams erreicht ist.<br />

Auf den ersten Blick sieht es so aus, als wenn sich die while-Schleife die Sache<br />

unnötig kompliziert machen würde: Anstatt den Bytepuffer einfach an eine Zeichenkette<br />

anzuhängen, verwendet sie ein StringBuilder-Objekt. Zur Erklärung<br />

sehen Sie sich bitte einmal das folgende Beispiel an:<br />

strMyString = strMyString + "und noch etwas Text";<br />

Ganz offensichtlich geht es hier um das Kopieren von Werten: Der konstante Wert<br />

"und noch etwas Text" wird implizit in ein string-Objekt konvertiert (Stichwort:<br />

Boxing), und für das Aneinanderhängen der beiden Zeichenketten ist ein weiteres<br />

string-Objekt fällig, das dann schließlich strMyString zugewiesen wird. Anders


Kapitel 8 • Komponenten mit C# schreiben 125<br />

gesagt: Sowohl die anzuhängende Zeichenfolge als auch der vorherige Wert von<br />

strMyString werden mehrfach kopiert.<br />

Schön wäre es natürlich, wenn<br />

strMyString += "und noch etwas Text";<br />

sich mit weniger Kopieraktionen begnügen würde – was aber in C# leider nicht<br />

der Fall ist: Der (entsprechend überladene) Operator += ist für Zeichenketten<br />

exakt so implementiert wie zuvor beschrieben.<br />

Und genau dies rechtfertigt den Einsatz der Klasse StringBuilder. Sie verwendet<br />

für das Anhängen, Einfügen, Herausnehmen und Ersetzen von Zeichen einen einzigen<br />

Puffer und kommt mit einem Minimum an Kopieroperationen aus.<br />

Womit eigentlich nur noch ein einziges interessantes Detail dieser Klasse unerwähnt<br />

geblieben wäre: Die Umsetzung der Codierung in Zeile 45. Encoding.<br />

ASCII sorgt schlicht dafür, dass das Programm die Zeichen in dem Zeichensatz zu<br />

sehen bekommt, den es anfordert.<br />

Nach dem Einlesen und Konvertieren des gesamten Streams fordert die Methode<br />

schließlich den Wert des StringBuilder-Objekts als Zeichenkette an und weist<br />

ihn der Ausgangsvariablen zu. Alternativ hätte man dieser Methode auch ein<br />

direktes Funktionsergebnis vom Typ string geben können – allerdings nur um<br />

den Preis einer weiteren Kopieraktion.<br />

8.1.2 Kompilieren von Komponenten<br />

Obwohl die Klasse RequestWebPage eine Komponente werden soll, unterscheidet<br />

sich der Quelltext in keiner Weise von dem für eine Klasse, die ausschließlich<br />

innerhalb einer einzigen Anwendung existiert. Tatsächlich wird der Speicherort<br />

einer Klasse erst bei der Kompilierung festgelegt. Der folgende Aufruf erzeugt<br />

eine Bibliothek anstelle einer Anwendung:<br />

csc /r:System.Net.dll /t:library /out:wrq.dll requestwebpage.cs<br />

Der Schalter /t:library weist den C#-Compiler an, eine Bibliothek zu erstellen,<br />

ohne nach einer statischen Methode Main zu suchen. Da hier erneut der Namensraum<br />

System.Net verwendet wird, muss die dazugehörige Bibliothek System.<br />

Net.dll explizit über den Schalter /r: angegeben werden.<br />

Die bei dieser Kompilierung erzeugte Bibliothek wrq.dll lässt sich nun ähnlich<br />

wie System.Net.dll in Anwendungen einsetzen. Da es in diesem Kapitel ausschließlich<br />

um private Komponenten geht, können Sie sich das Kopieren von<br />

wrq.dll in einen speziellen Ordner sparen – es reicht, wenn diese Datei im selben<br />

Verzeichnis zu finden ist wie die Anwendung, die sie einsetzen.


126<br />

Die erste Komponente<br />

8.1.3 Erstellen einer einfachen Client-Anwendung<br />

Nachdem die Komponente geschrieben und fehlerfrei kompiliert worden ist, fehlt<br />

nur noch ein Anwendungsbeispiel dafür. Das folgende Listing zeigt ein einfaches,<br />

kommandozeilenorientiertes Programm, das zur Abfrage einer vom Autor unterhaltenen<br />

Webseite verwendbar ist.<br />

Listing 8.2:<br />

Einsatz der Klasse RequestWebPage zum Abfragen einer einfach<br />

gehaltenen Webseite<br />

1: using System;<br />

2:<br />

3: class TestWebReq<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: RequestWebPage wrq = new RequestWebPage();<br />

8: wrq.URL = "http://www.alphasierrapapa.com/iisdev/";<br />

9:<br />

10: string strResult;<br />

11: try<br />

12: {<br />

13: wrq.GetContent(out strResult);<br />

14: }<br />

15: catch (Exception e)<br />

16: {<br />

17: Console.WriteLine(e);<br />

18: return;<br />

19: }<br />

20:<br />

21: Console.WriteLine(strResult);<br />

22: }<br />

23: }<br />

Beachten Sie, dass der Aufruf von GetContent durch try...catch geklammert ist:<br />

Zum einen kann die Methode selbst eine Ausnahme des Typs ArgumentException<br />

signalisieren, zum anderen reagieren die von GetContent verwendeten NGWS-<br />

Klassen auf Probleme ihrerseits mit Ausnahmen. Da die Klasse RequestWebPage<br />

Ausnahmen nicht selbst behandelt, muss sich das Programm darum kümmern.<br />

Der Rest des Codes bedarf keiner ausgiebigen Erklärung mehr: Main ruft den<br />

Standardkonstruktor der Klasse auf, setzt die Eigenschaft URL des neu angelegten<br />

Objekts und benutzt dann seine Methode GetContent. Interessanter ist da schon<br />

der Aufruf des Compilers – ihm müssen Sie schließlich mitteilen, in welcher<br />

Bibliothek er die Klasse RequestWebPage suchen soll:<br />

csc /r:wrq.dll testwebreq.cs


Kapitel 8 • Komponenten mit C# schreiben 127<br />

Mehr bleibt nicht zu tun, einmal abgesehen von der recht bescheidenen Darstellungsform:<br />

Die gelesenen Daten huschen ohne weitere Interpretation schlicht<br />

über das Konsolenfenster. Natürlich steht es Ihnen frei, einen Parser für den<br />

HTML-Code der Webseite zu schreiben, der Spezifisches extrahiert, und das<br />

Ganze schließlich als weitere Komponente in Ihrer eigenen Bibliothek zu speichern.<br />

Dem Autor schwebt eine SSL-taugliche Version der Komponente vor, die<br />

sich für die Online-Verifizierung von Kreditkartennummern in ASP+-Webseiten<br />

einsetzen lässt.<br />

Es dürfte Ihnen aufgefallen sein, dass das Beispielprogramm ohne ein eigenes<br />

using für die verwendete Bibliothek auskommt. Das liegt daran, dass der Quelltext<br />

der Komponente keinen eigenen Namensraum definiert. Womit wir beim<br />

zweiten Hauptthema dieses Kapitels angekommen wären.<br />

8.2 Namensräume<br />

Namensräume wie System und System.Net sind Ihnen im Rahmen der vorangehenden<br />

Beispiele dieses Buches bereits des Öfteren begegnet. C# verwendet<br />

Namensräume zur Organisation von Programmen, wobei es die hierarchische<br />

Natur dieser Organisation einfach macht, Elemente eines Programms anderen<br />

Programmen zur Verfügung zu stellen. Tatsächlich muss es nicht einmal um die<br />

öffentliche Darstellung gehen: Namensräume erleichtern nämlich auch die interne<br />

Organisation von Anwendungen.<br />

Obwohl Sie C# nicht dazu zwingt, sollten Sie die Hierarchie innerhalb von<br />

Anwendungen grundsätzlich durch Namensräume deutlich machen. Das NGWS-<br />

Subsystem ist ein gutes Beispiel dafür, wie eine solche Hierarchie aufgebaut sein<br />

kann.<br />

Der folgende Codeauszug zeigt die Definition eines einfachen Namensraums<br />

My.Test (wobei der Punkt die Hierarchieebenen unterscheidet) in einem C#-<br />

Quelltext:<br />

namespace My.Test<br />

{<br />

// Definitionen und Deklarationen in diesem Namensraum<br />

}<br />

Zugriffe auf Elemente eines Namensraums sind sowohl über einen vollständig<br />

qualifizierten Bezeichner als auch über die Direktive using, die sämtliche Elemente<br />

des angegebenen Namensraums in den aktuellen Namensraum importiert,<br />

möglich. Beide Techniken finden Sie in den vorangehenden Beispielen dieses<br />

Buchs.<br />

Bevor Sie nun selbst Namensräume definieren, noch ein paar Worte zur Verfügbarkeit<br />

der Bezeichner. Solange Sie keinen Modifizierer für die Verfügbarkeit


128<br />

Namensräume<br />

angeben, setzt der Compiler als Standardvorgabe internal ein. Soll ein Bezeichner<br />

öffentlich verfügbar sein, verwenden Sie das Schlüsselwort public. Andere<br />

Modifizierer für die Verfügbarkeit sind in diesem Zusammenhang nicht erlaubt.<br />

Und damit wieder einmal genug der Theorie. In den folgenden Abschnitten geht<br />

es um praktische Beispiele für die folgenden Techniken:<br />

• Einbetten einer Klasse in einen Namensraum<br />

• Einsatz eigener Namensräume in einer Anwendung<br />

• Mehrere Klassen mit gemeinsamem Namensraum<br />

8.2.1 Einbetten einer Klasse in einen Namensraum<br />

Zur Praxisdemonstration selbst definierter Namensräume bietet sich der englische<br />

Originaltitel dieses Buchs an; Um Sie aber nicht mit dem schlichten Verpacken<br />

der bereits diskutierten Komponente RequestWebPage in einen Namensraum Presenting.CSharp<br />

zu langweilen, stellt das folgende Beispiel eine komplett neue<br />

Klasse namens WhoisLookup vor, mit der sich Whois-Informationen abrufen lassen.<br />

Listing 8.3:<br />

Implementation der Klasse WhoisLookup in einem eigenen<br />

Namensraum<br />

1: using System;<br />

2: using System.Net.Sockets;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: namespace Presenting.CSharp<br />

7: {<br />

8: public class WhoisLookup<br />

9: {<br />

10: public static bool Query(string strDomain, out string strWhoisInfo)<br />

11: {<br />

12: const int BUFFER_SIZE = 128;<br />

13:<br />

14: if ("" == strDomain)<br />

15: throw new ArgumentException("Domainname fehlt");<br />

16:<br />

17: TCPClient tcpc = new TCPClient();<br />

18: strWhoisInfo = "N/A";<br />

19:<br />

20: // Verbindungsversuch zum Whois-Server<br />

21: if (tcpc.Connect("whois.networksolutions.com", 43) != 0)<br />

22: return false;<br />

23:


Kapitel 8 • Komponenten mit C# schreiben 129<br />

24: // Stream der TCP-Verbindung<br />

25: Stream s = tcpc.GetStream();<br />

26:<br />

27: // Anfrage senden<br />

28: strDomain += "\r\n";<br />

29: Byte[] bDomArr = Encoding.ASCII.GetBytes(strDomain.ToChar-<br />

Array());<br />

30: s.Write(bDomArr, 0, strDomain.Length);<br />

31:<br />

32: Byte[] Buffer = new Byte[BUFFER_SIZE];<br />

33: StringBuilder strWhoisResponse = new StringBuilder("");<br />

34:<br />

35: int BytesRead = s.Read(Buffer, 0, BUFFER_SIZE);<br />

36: while (BytesRead != 0 )<br />

37: {<br />

38: strWhoisResponse.Append(Encoding.ASCII.GetString(Buffer, 0,<br />

BytesRead));<br />

39: BytesRead = s.Read(Buffer, 0, BUFFER_SIZE);<br />

40: }<br />

41:<br />

42: tcpc.Close();<br />

43: strWhoisInfo = strWhoisResponse.ToString();<br />

44: return true;<br />

45: }<br />

46: }<br />

47: }<br />

Der Namensraum Presenting.CSharp wird in Zeile 6 deklariert; er umschließt die<br />

Klasse WhoisLookup mit geschweiften Klammern in den Zeilen 7 und 47. Falls Sie<br />

nun noch auf weitere Erklärungen warten: Mehr ist zur Definition eines neuen<br />

Namensraums wirklich nicht zu tun.<br />

Der Code von WhoisLookup verdient dagegen durchaus noch einige Worte – vor<br />

allem, weil er zeigt, wie einfach sich in C# Sockets einsetzen lassen. Nach der<br />

(mit Sicherheit verbesserungswürdigen) Prüfung des Domainnamens legt die statische<br />

Methode Query ein TCPClient-Objekt an, das die gesamte Kommunikation<br />

mit dem Whois-Server über den Port 43 übernimmt. Die Verbindung zum Server<br />

wird in Zeile 21 hergestellt:<br />

if (tcpc.Connect("whois.networksolutions.com", 43) != 0)<br />

Da das Fehlschlagen dieses Verbindungsversuchs sehr wohl zu erwarten ist, reagiert<br />

die Methode darauf nicht mit einer Ausnahme – erinnern Sie sich noch an<br />

die im vorangehenden Kapitel genannten Regeln? Das direkte Funktionsergebnis<br />

ist ein schlichter Fehlercode; 0 steht dagegen für eine zustande gekommene Verbindung.


130<br />

Namensräume<br />

Whois-Abfragen setzen erst einmal das Senden von Daten voraus – nämlich des<br />

Domainnamens, zu dem Informationen gewünscht werden. In Zeile 25 holt sich<br />

die Methode deshalb eine Referenz auf den (bidirektionalen) Stream der TCP-<br />

Verbindung. Der Domainname bekommt ein CR/LF-Paar angehängt, um das<br />

Ende der Anfrage zu markieren, wird in ein Byte-Array umgepackt und dann zum<br />

Whois-Server gesendet (Zeile 30).<br />

Der Rest des Codes dürfte Ihnen von der Klasse RequestWebPage her recht<br />

bekannt vorkommen: Eine while-Schleife, in der die Antwort des Servers in einen<br />

Puffer eingelesen wird, und ein StringBuilder-Objekt, das die einzelnen Pufferinhalte<br />

aneinander hängt. Nach dem Ende der Leseaktion wird die Verbindung<br />

explizit über Close geschlossen, bevor die Methode das Ergebnis an den Aufrufer<br />

weiterreicht. Im Prinzip könnte man das auch dem Garbage Collector überlassen<br />

– nur gehören TCP-Verbindungen zu den Ressourcen, die man keine Millisekunde<br />

länger belegen sollte als unbedingt nötig.<br />

Bevor Sie diese Klasse in einer Anwendung einsetzen können, müssen Sie sie als<br />

Komponente zu einer Bibliothek kompilieren. Obwohl der Quelltext einen<br />

Namensraum definiert, enthält die dafür notwendige Kommandozeile keine neuen<br />

Elemente:<br />

csc /r:System.Net.dll /t:library /out:whois.dll whoislookup.cs<br />

Wenn Sie den Quelltext unter dem Namen whois.cs speichern, können Sie sich<br />

den Schalter /out: im Prinzip sparen, weil Bibliotheken als Standardvorgabe denselben<br />

Namen wie der C#-Quelltext (nur eben mit der Erweiterung .dll) bekommen.<br />

Dass man /out: auch in einem solchen Fall angeben sollte, ist eher eine<br />

Frage sinnvoller Gewohnheiten. Die meisten Projekte bestehen aus mehreren<br />

Quelltextdateien, und ohne explizite Festlegung per /out: verwendet der Compiler<br />

für die Bibliothek dann einfach den Namen des zuerst angegebenen Quelltexts.<br />

8.2.2 Einsatz eigener Namensräume in einer Anwendung<br />

Da die Komponente einen eigenen Namensraum hat, muss eine Anwendung diesen<br />

Namensraum entweder mit<br />

using Presenting.CSharp;<br />

importieren oder für seine Elemente vollständig qualifizierte Bezeichner verwenden<br />

– beispielsweise:<br />

Presenting.CSharp.WhoisLookup.Query(...);


Kapitel 8 • Komponenten mit C# schreiben 131<br />

So lange keine Konflikte durch gleichnamige Bezeichner verschiedener Namensräume<br />

zu erwarten sind, sollte dem Import mit using der Vorzug gegeben werden<br />

– nicht nur, weil das die Tipparbeit kräftig reduziert. Das folgende Listing zeigt<br />

einen einfachen Client der neuen Komponente.<br />

Listing 8.4:<br />

Eine Beispielanwendung für die Komponente WhoisLookup<br />

1: using System;<br />

2: using Presenting.CSharp;<br />

3:<br />

4: class TestWhois<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: string strResult;<br />

9: bool bReturnValue;<br />

10:<br />

11: try<br />

12: {<br />

13: bReturnValue = WhoisLookup.Query("microsoft.com", out<br />

strResult);<br />

14: }<br />

15: catch (Exception e)<br />

16: {<br />

17: Console.WriteLine(e);<br />

18: return;<br />

19: }<br />

20: if (bReturnValue)<br />

21: Console.WriteLine(strResult);<br />

22: else<br />

23: Console.WriteLine("Keine Informationen vom Whois-Server<br />

erhalten.");<br />

24: }<br />

25: }<br />

In Zeile 2 wird der Namensraum Presenting.CSharp mit using importiert. Darauf<br />

folgende Anweisungen, die sich auf die Klasse WhoisLookup beziehen, kommen<br />

deshalb ohne explizite Angabe dieses Namensraums aus.<br />

Das Programm fragt die Whois-Informationen für die Domain microsoft.com ab,<br />

wobei sich dieser Domainname im Quelltext auch durch einen Namen Ihrer Wahl<br />

ersetzen lässt. Tatsächlich wäre es natürlich sinnvoller, wenn man den Domainnamen<br />

beim Start des Programms über die Kommandozeile angeben könnte. Das<br />

folgende Listing zeigt eine entsprechende Variante des Programms, die der Kürze<br />

halber auf eine Ausnahmebehandlung verzichtet:


132<br />

Namensräume<br />

Listing 8.5:<br />

Übergabe eines Kommandozeilenparameters an die Methode Query<br />

1: using System;<br />

2: using Presenting.CSharp;<br />

3:<br />

4: class WhoisShort<br />

5: {<br />

6: public static void Main(string[] args)<br />

7: {<br />

8: string strResult;<br />

9: bool bReturnValue;<br />

10:<br />

11: bReturnValue = WhoisLookup.Query(args[0], out strResult);<br />

12:<br />

13: if (bReturnValue)<br />

14: Console.WriteLine(strResult);<br />

15: else<br />

16: Console.WriteLine("Whois-Anfrage gescheitert.");<br />

17: }<br />

18: }<br />

Sie kompilieren dieses kleine Programm wie gehabt mit:<br />

csc /r:whois.dll whoisshort.cs<br />

Der Name der abzufragenden Domain wird nun beim Start des Programms in der<br />

Kommandozeile angegeben. Für die Abfrage von microsoft.com beispielsweise:<br />

whoisclnt microsoft.com<br />

Welche Registrierungsinformationen bei einer fehlerfreien Abfrage für microsoft.com<br />

herauskommen, gibt das nächste Listing in leicht verkürzter Fassung<br />

wieder. Tatsächlich ist whoisshort ein recht nützliches Werkzeug, das komponentenbasiert<br />

arbeitet und insgesamt in weniger als einer Stunde geschrieben wurde.<br />

Wie lange das wohl mit C++ gedauert hätte?<br />

Listing 8.6:<br />

Whois-Information über microsoft.com (gekürzt)<br />

D:\CSharp\Samples\Namespace>whoisshort<br />

...<br />

Registrant:<br />

Microsoft Corporation (MICROSOFT-DOM)<br />

1 microsoft way<br />

redmond, WA 98052<br />

US<br />

Domain Name: MICROSOFT.COM


Kapitel 8 • Komponenten mit C# schreiben 133<br />

Administrative Contact:<br />

Microsoft Hostmaster (MH37-ORG) msnhst@MICROSOFT.COM<br />

Technical Contact, Zone Contact:<br />

MSN NOC (MN5-ORG) msnnoc@MICROSOFT.COM<br />

Billing Contact:<br />

Microsoft-Internic Billing Issues (MDB-ORG) msnbill@MICRO-<br />

SOFT.COM<br />

Record last updated on 20-May-2000.<br />

Record expires on 03-May-2010.<br />

Record created on 02-May-1991.<br />

Database last updated on 9-Jun-2000 13:50:52 EDT.<br />

Domain servers in listed order:<br />

ATBD.MICROSOFT.COM 131.107.1.7<br />

DNS1.MICROSOFT.COM 131.107.1.240<br />

DNS4.CP.MSFT.NET 207.46.138.11<br />

DNS5.CP.MSFT.NET 207.46.138.12<br />

8.2.3 Mehrere Klassen mit gemeinsamem Namensraum<br />

Wenn Sie die Klassen WhoisLookup und RequestWebPage beide im Namensraum<br />

Presenting.CSharp unterbringen wollen, haben Sie nicht allzu viel Arbeit vor<br />

sich. Da WhoisLookup bereits Teil dieses Namensraums ist, beschränken sich die<br />

Änderungen auf die Datei requestwebpage.cs und bestehen aus wenigen Zeilen:<br />

namespace Presenting.CSharp<br />

{<br />

public class RequestWebPage<br />

{<br />

...<br />

}<br />

}<br />

Obwohl die beiden Klassen in getrennten Dateien untergebracht sind, werden sie<br />

durch den folgenden Aufruf des Compilers in einen Namensraum zusammengefasst:<br />

csc /r:System.Net.dll /t:library /out:presenting.csharp.dll whoislookup.cs<br />

requestwebpage.cs<br />

Der in diesem Beispiel mit /out: festgelegte Name der DLL ist nicht zwingend.<br />

Im allgemeinen verwendet man für solche Bibliotheken aber den Namensraum<br />

auch als Dateinamen, weil sich dann der Zusammenhang zwischen using-Direktiven<br />

und bei der Kompilierung anzugebenden Bibliotheken von selbst ergibt.


134<br />

Zusammenfassung<br />

8.3 Zusammenfassung<br />

In diesem Kapitel ging es um das Erstellen von Komponenten – also Klassen, die<br />

sich nicht nur in einer, sondern in einer beliebigen Zahl von Anwendungen (Clients)<br />

einsetzen lassen. Die Vergabe von Namensräumen für solche Komponenten<br />

ist nicht zwingend, hilft aber sowohl beim Aufbau einer Klassenhierarchie als<br />

auch bei der internen Organisation einzelner Anwendungen.<br />

Komponenten lassen sich in C# ausgesprochen einfach erstellen. Installiert werden<br />

müssen sie nicht: Es reicht, wenn sich die Bibliothek im selben Verzeichnis<br />

befindet wie ihre Clients. Dieses Bild ändert sich allerdings, wenn es um Klassenhierarchien<br />

geht, die überall zur Verfügung stehen sollen. Mit dem Wie und<br />

Warum beschäftigt sich das nächste Kapitel.


Kapitel 9<br />

Konfiguration und Verbreitung<br />

9.1 Bedingtes Kompilieren 136<br />

9.2 Kommentarbasierte Dokumentation in XML 142<br />

9.3 Versionspflege 156<br />

9.4 Zusammenfassung 159


136<br />

Bedingtes Kompilieren<br />

Das vorige Kapitel hat Ihnen gezeigt, wie Sie eine Komponente implementieren<br />

und im Rahmen einer einfachen Testanwendung einsetzen. Obwohl die vorgestellte<br />

Komponente vom Prinzip her fertig ist, sollte man ihr noch die eine oder<br />

andere Technik angedeihen lassen.<br />

• Bedingtes Kompilieren<br />

• Kommentarbasierte Dokumentation in XML<br />

• Versionspflege<br />

9.1 Bedingtes Kompilieren<br />

Ein Feature, auf das die wenigsten Programmierer verzichten wollen, ist die<br />

bedingte Kompilation. Diese Technik eröffnet die Möglichkeit, Teile des Codes<br />

bedingt ein- und auszublenden, etwa um Debug-, Demo- oder Release-Versionen<br />

einer Anwendung auf Grundlage ein und desselben Quelltexts zu generieren.<br />

C# unterstützt zwei unterschiedliche Formen der bedingten Übersetzung:<br />

• Einsatz des Präprozessors<br />

• conditional-Attribut<br />

9.1.1 Einsatz des Präprozessors<br />

In C++ verkörpert der Präprozessor einen separaten Lauf über den Quellcode, der<br />

der eigentlichen Kompilierung vorgeschaltet ist. Anders in C#: Hier »emuliert«<br />

der Compiler den Präprozessor, und es gibt auch keinen separaten Lauf. Der Präprozessor<br />

von C# ist nicht mehr (und nicht weniger) als ein Mechanismus für die<br />

bedingte Kompilierung!<br />

Obwohl der C#-Compiler keine Makros unterstützt, kennt er alle notwendigen<br />

Anweisungen, um das bedingte Ein- und Ausblenden von Code auf Basis von<br />

Symboldefinitionen zu ermöglichen. Die folgenden Abschnitte stellen Ihnen die<br />

entsprechenden C#-Direktiven vor, die denen von C++ recht ähnlich sind.<br />

• Definition von Symbolen<br />

• Code mit Hilfe von Symbolen ein- und ausblenden<br />

• Fehlermeldungen und Warnungen generieren<br />

Definition von Symbolen<br />

Makros lassen mit der Präprozessorfunktionalität von C# nicht definieren, wohl<br />

aber Symbole. Darüber hinaus kann der Präprozessor bestimmte Codeanteile einund<br />

ausblenden, je nachdem, ob ein bestimmtes Symbol definiert ist oder nicht.


Kapitel 9 • Konfiguration und Verbreitung 137<br />

Die eine Möglichkeit, ein Symbol zu definieren, besteht darin, eine #define-<br />

Direktive dafür an den Anfang der C#-Quelldatei zu setzen:<br />

#define DEBUG<br />

Diese Zeile definiert das Symbol DEBUG für den gesamten Bereich der Quelldatei.<br />

Beachten Sie, dass Symboldefinitionen vor allen anderen Anweisungen zu treffen<br />

sind. So nötigt der folgende Codeauszug den Compiler beispielsweise zu einer<br />

Fehlermeldung:<br />

using System;<br />

#define DEBUG<br />

Weiterhin kann man auch den Compiler für die Symboldefinition benutzen,<br />

indem man den Compilerschalter /define verwendet. Die so definierten Symbole<br />

sind dann in allen Quelldateien bekannt, die der jeweilige Lauf umfasst:<br />

csc /define:DEBUG mysymbols.cs<br />

Um mehrere Symbole mit dem Compiler zu definieren, setzen Sie die Symbole<br />

hintereinander, durch ein Semikolon getrennt:<br />

csc /define:RELEASE;DEMOVERSION mysymbols.cs<br />

Die gleiche Definition lässt sich natürlich auch auf Ebene einer einzelnen C#-<br />

Quelldatei treffen. Dazu ergänzen Sie je Symbol eine #define-Direktive in einer<br />

neuen Zeile.<br />

Falls ein über den Compiler definiertes Symbol in einer bestimmten Quelldatei<br />

unbekannt bleiben soll, ergänzen Sie für das Symbol eine #undef-Direktive:<br />

#undef DEBUG<br />

Für #undef-Direktiven gelten dieselben Regeln wie für #define-Direktiven: Ihre<br />

Wirkung bleibt auf die jeweilige Quelldatei beschränkt, und sie müssen vor der<br />

ersten Anweisung in einer eigenen Zeile stehen.<br />

Viel mehr gibt es über die Symboldefinition mit dem C#-Präprozessor nicht zu<br />

sagen. Die folgenden Abschnitte zeigen, wie man Symbole für die bedingte Übersetzung<br />

von Code einsetzt.<br />

Code mit Hilfe von Symbolen ein- und ausblenden<br />

Die Definition von Symbolen dient in erster Linie dem Zweck, Teile des Codes<br />

bedingt ein- und ausblenden zu können. Das folgende Listing zeigt die Variante<br />

eines bereits in Kapitel 5 vorgestellten Programms, das in Abhängigkeit von dem<br />

Symbol CALC_W_OUT_PARAM bedingt kompiliert wird:


138<br />

Bedingtes Kompilieren<br />

Listing 9.1:<br />

Mit der #if-Directive bedingt kompilieren<br />

1: using System;<br />

2:<br />

3: public class SquareSample<br />

4: {<br />

5: public void CalcSquare(int nSideLength, out int nSquared)<br />

6: {<br />

7: nSquared = nSideLength * nSideLength;<br />

8: }<br />

9:<br />

10: public int CalcSquare(int nSideLength)<br />

11: {<br />

12: return nSideLength * nSideLength;<br />

13: }<br />

14: }<br />

15:<br />

16: class SquareApp<br />

17: {<br />

18: public static void Main()<br />

19: {<br />

20: SquareSample sq = new SquareSample();<br />

21:<br />

22: int nSquared = 0;<br />

23:<br />

24: #if CALC_W_OUT_PARAM<br />

25: sq.CalcSquare(20, out nSquared);<br />

26: #else<br />

27: nSquared = sq.CalcSquare(15);<br />

28: #endif<br />

29: Console.WriteLine(nSquared.ToString());<br />

30: }<br />

31: }<br />

Nachdem das Symbol CALC_W_OUT_PARAM im Quelltext nicht definiert ist, muss es<br />

– je nach gewünschter Version – über den Compiler definiert werden (oder eben<br />

nicht):<br />

csc /define:CALC_W_OUT_PARAM squaresample.cs<br />

Ist das Symbol bekannt, übersetzt der Compiler den Aufruf der Methode Calc-<br />

Square in Zeile 25, ansonsten den in Zeile 27. Die vom C#-Compiler emulierten<br />

Präprozessordirektiven #if, #else und #endif verhalten sich von der Logik her<br />

wie die if…else-Anweisung der Sprache, mit dem Unterschied, dass der Compiler<br />

den false-Zweig gar nicht erst zu sehen bekommt. Bei Bedarf können Sie<br />

auch die Operatoren && und || sowie ! auf Präprozessorebene einsetzen. Das folgende<br />

Listing zeigt ein Beispiel:


Kapitel 9 • Konfiguration und Verbreitung 139<br />

Listing 9.2:<br />

Erweiterte Fallunterscheidung auf Präprozessorebene mit der #elif-<br />

Direktive<br />

1: // #define DEBUG<br />

2: #define RELEASE<br />

3: #define DEMOVERSION<br />

4:<br />

5: #if DEBUG<br />

6: #undef DEMOVERSION<br />

7: #endif<br />

8:<br />

9: using System;<br />

10:<br />

11: class Demo<br />

12: {<br />

13: public static void Main()<br />

14: {<br />

15: #if DEBUG<br />

16: Console.WriteLine("Debug-Version");<br />

17: #elif RELEASE && !DEMOVERSION<br />

18: Console.WriteLine("Vollversion");<br />

19: #else<br />

20: Console.WriteLine("Demoversion");<br />

21: #endif<br />

22: }<br />

23: }<br />

In diesem Beispiel sind alle Symboldefinitionen im Quelltext enthalten. Achten<br />

Sie auf die bedingte #undef-Direktive in Zeile 6. Da der Fall »Debug-Version für<br />

Demo-Version« als eigenständiger Übersetzungslauf nicht vorgesehen ist,<br />

schließt ihn die Präprozessorlogik aus – und zwar so, dass er sich auch dann nicht<br />

über die Hintertür einschleichen kann, wenn der Compiler das Symbol DEBUG definiert.<br />

Die Fallunterscheidung des Präprozessors findet in den Zeilen 15 bis 21 statt. Um<br />

mehr als zwei Fälle unterscheiden zu können, empfiehlt sich der Einsatz der<br />

#elif-Direktive sowie des &&-Operators. An Operatoren versteht der Präprozessor<br />

neben den bereits genannten logischen Operatoren auch die Vergleichsoperatoren<br />

== und !=.<br />

Fehlermeldungen und Warnungen generieren<br />

Weitere Präprozessordirektiven sind #warning und #error. Wie zu erwarten, veranlassen<br />

diese Direktiven den Compiler zur Ausgabe einer Warnung bzw. Fehlermeldung<br />

mit dem auf die Direktive folgenden Text. Es versteht sich, dass die Ausgabe<br />

solcher Meldungen im Allgemeinen bedingt erfolgen wird. Listing 9.3 zeigt<br />

den Einsatz der beiden Direktiven:


140<br />

Bedingtes Kompilieren<br />

Listing 9.3:<br />

Compilerwarnungen und Fehlermeldungen mittels<br />

Präprozessordirektiven ausgeben<br />

1: #define DEBUG<br />

2: #define RELEASE<br />

3: #define DEMOVERSION<br />

4:<br />

5: #if DEMOVERSION && !DEBUG<br />

6: #warning Kompiliere Demoversion<br />

7: #endif<br />

8:<br />

9: #if DEBUG && DEMOVERSION<br />

10: #error Die Debugversion einer Demoversion ist nicht vorgesehen<br />

11: #endif<br />

12:<br />

13: using System;<br />

14:<br />

15: class Demo<br />

16: {<br />

17: public static void Main()<br />

18: {<br />

19: Console.WriteLine("Demoanwendung");<br />

20: }<br />

21: }<br />

Bei der Übersetzung dieses Programms generiert der Compiler regulär eine Warnung,<br />

wenn das Symbol DEMOVERSION definiert ist und das Symbol DEBUG nicht<br />

(Zeilen 5 bis 7). Falls beide Symbole definiert sind, resultiert eine Fehlermeldung,<br />

die gleichzeitig verhindert, dass der Compiler ausführbaren Code generiert. Verglichen<br />

mit dem vorigen Beispiel, das die Definition des nicht erwarteten Symbols<br />

DEBUG kommentarlos ungeschehen macht, informiert Sie dieser Code über die<br />

Situation. Das ist zweifellos der bessere Ansatz.<br />

9.1.2 conditional-Attribut<br />

Die wichtigste Aufgabe des Präprozessors von C++ ist wahrscheinlich die Definition<br />

von Makros, die in Abhängigkeit von Symboldefinitionen in bestimmten<br />

Übersetzungsläufen Code einflechten, in anderen nicht. Beispiele dafür sind etwa<br />

die Makros ASSERT und TRACE, die zu Funktionsaufrufen werden, wenn das Symbol<br />

DEBUG definiert ist, und zu Nichts evaluieren, wenn es um die Übersetzung von<br />

Release-Versionen geht.<br />

Aus der Tatsache, dass der C#-Präprozessor keine Makros unterstützt, werden Sie<br />

vielleicht folgern, dass es in C# generell keine bedingte Funktionalität in dieser<br />

Art gibt. Erfreulicherweise ist dem aber nicht so. C# unterstützt ein conditional-<br />

Attribut für die Deklaration von Methoden, das genau den gewünschten Effekt<br />

hat:


Kapitel 9 • Konfiguration und Verbreitung 141<br />

[conditional("DEBUG")]<br />

public void SomeMethod() { }<br />

Die Methode SomeMethod ist für den Compiler nur dann im Code sichtbar, wenn<br />

das Symbol DEBUG definiert ist. Das gilt natürlich nicht nur für ihre Definition,<br />

sondern insbesondere auch für alle Aufrufe. Mit anderen Worten, der Compiler<br />

übergeht Aufrufe wie<br />

SomeMethod();<br />

wenn die Methode mit dem conditional-Attribut für ein bestimmtes Symbol<br />

deklariert wurde und das Symbol nicht bekannt ist. Wie man sich leicht überlegt,<br />

kann die Eliminierung von Funktionsaufrufen aus dem Code nur dann ohne weitere<br />

Schwierigkeiten geschehen, wenn für die Methode der Ergebnistyp void vereinbart<br />

wurde – und genau dies ist Voraussetzung für das conditional-Attribut.<br />

Hinsichtlich der Parameter gibt es jedoch keine Einschränkungen.<br />

Das folgende Listing zeigt, wie sich das conditional-Attribut einsetzen lässt, um<br />

die Funktionalität des aus C++ bekannten ASSERT-Makros in C# nachzubilden.<br />

Der Einfachheit halber geht die Ausgabe an die Konsole – man könnte sie ohne<br />

weiteres aber auch woandershin dirigieren, beispielsweise in eine Datei.<br />

Listing 9.4:<br />

Implementation der TRACE-Funktionalität mit dem conditional-<br />

Attribut<br />

1: #define DEBUG<br />

2:<br />

3: using System;<br />

4:<br />

5: class Info<br />

6: {<br />

7: [conditional("DEBUG")]<br />

8: public static void Trace(string strMessage)<br />

9: {<br />

10: Console.WriteLine(strMessage);<br />

11: }<br />

12:<br />

13: [conditional("DEBUG")]<br />

14: public static void TraceX(string strFormat,params object[] list)<br />

15: {<br />

16: Console.WriteLine(strFormat, list);<br />

17: }<br />

18: }<br />

19:<br />

20: class TestConditional<br />

21: {<br />

22: public static void Main()<br />

23: {


142<br />

Kommentarbasierte Dokumentation in XML<br />

24: Info.Trace("Cool!");<br />

25: Info.TraceX("{0} {1} {2}", "C", "U", 2001);<br />

26: }<br />

27: }<br />

Die Klasse Info enthält zwei bedingte, auf das DEBUG-Symbol zugeschnittene Vereinbarungen<br />

für statische Methoden: Trace, eine einparametrige Methode, und<br />

TraceX, eine Methode, die mit einer flexiblen Anzahl von Parametern aufgerufen<br />

werden kann. Über Trace gibt es weiter nichts zu sagen. Die Deklaration von TraceX<br />

enthält hingegen den Zusatz params, den Sie vielleicht von Visual Basic her<br />

kennen werden.<br />

Indem Sie das Schlüsselwort params vor einen Arraybezeichner setzen, kann die<br />

Methode eine beliebige Anzahl von Argumenten verarbeiten – vergleichbar mit<br />

der Bedeutung der Ellipse »…« in C++. Zu beachten ist lediglich, dass Sie params<br />

nur einmal und auch nur für den letzten Parameter in der Parameterliste einer<br />

Methode verwenden dürfen. Beide Beschränkungen dürften sich aber von selbst<br />

verstehen.<br />

Hinter der Verwendung des params-Schlüsselworts steckt natürlich die Absicht,<br />

eine Trace-Methode zu erhalten, deren Parameterschema, eine Formatzeichenfolge<br />

in Kombination mit einer offenen Anzahl an Parameterwerten, genau auf<br />

den Aufruf der WriteLine-Methode passt (Zeile 16).<br />

Die Ausgabe des kleinen Programms wird ausschließlich durch das Symbol DEBUG<br />

bestimmt. Ist das Symbol bekannt, übersetzt der Compiler die beiden Methoden<br />

sowie ihre Aufrufe in der Methode Main, andernfalls nicht.<br />

Bedingt definierte Methoden sind ein wirklich mächtiges Werkzeug, das es Ihnen<br />

erlaubt, Ihre Anwendungen und Komponenten auf sehr einfache Weise mit<br />

bedingter Funktionalität auszustatten. Mit ein paar zusätzlichen Schnörkeln können<br />

Sie auch bedingte Methoden vereinbaren, die von mehreren Symbolen – verbunden<br />

durch die logischen Operatoren && und || – abhängig sind. Details darüber<br />

finden Sie in der C#-Dokumentation.<br />

9.2 Kommentarbasierte Dokumentation in XML<br />

Eine Aufgabe, die den wenigsten Programmierern wirklich Spaß macht, ist das<br />

ausführliche Kommentieren und Dokumentieren ihrer Quelltexte. In C# steckt das<br />

Potenzial, mit alten Gewohnheiten zu brechen: Sie können sich die Dokumentation<br />

automatisiert aus den Kommentaren in Ihren Quelltexten generieren lassen.<br />

Da der Compiler die Ausgabe im XML-Format liefert, lässt sie sich sowohl als<br />

Eingabedatei für die weitergehende Dokumentation der Komponente verwenden<br />

als auch als Informationsquelle für Werkzeuge, die Hilfe und implementatorische<br />

Einblicke zu Ihren Komponenten verfügbar machen. Visual Studio 7 ist beispiels-


Kapitel 9 • Konfiguration und Verbreitung 143<br />

weise so ein Werkzeug. Gute Dokumentation kann nun endlich zu dem Verkaufsargument<br />

werden, das sie immer schon hätte sein sollen.<br />

Dieser Abschnitt will Ihnen zeigen, wie Sie den Dokumentationsmechanismus<br />

von C# optimal für sich einsetzen. Das Beispielmaterial ist bewusst einfach und<br />

ausführlich gehalten, um Ihnen die Ausrede zu nehmen, der Abschnitt sei zu<br />

kompliziert gewesen, als dass Sie hätten herausfinden können, wie man in einem<br />

C#-Quelltext Kommentare für die Dokumentation verwendet. Dokumentation ist<br />

ein immens wichtiger Bestandteil von Software, speziell von Komponenten, die<br />

auch andere Entwickler verwenden können sollen.<br />

In den folgenden Unterabschnitten lernen Sie am Beispiel der in Kapitel 8 vorgestellten<br />

Klasse RequestWebPage verschiedene Techniken kennen, wie Sie die einzelnen<br />

Bestandteile Ihrer Dokumentation als C#-Kommentare formulieren. Die<br />

entsprechenden Überschriften lauten:<br />

• Elemente beschreiben<br />

• Bemerkungen und Auflistungen einfügen<br />

• Beispiele anführen<br />

• Parameter beschreiben<br />

• Eigenschaften beschreiben<br />

• Kompilieren der Dokumenation<br />

9.2.1 Elemente beschreiben<br />

Im ersten Schritt fügen Sie die Beschreibung eines Klassenelements ein – und<br />

zwar mit einem -Tag:<br />

/// Dieses Element fungiert als ... <br />

Kommentare, die etwas zur Dokumentation beitragen, beginnen mit einem dreifachen<br />

Schrägstrich /// und werden vor das beschriebene Element gesetzt:<br />

/// Klasse fordert eine Webseite von einem Webserver an<br />

public class RequestWebPage<br />

Um einen Absatz in eine Beschreibung einzufügen, verwenden Sie das Tag-Paar<br />

und . Und das Tag ermöglicht Verweise auf andere Elemente:<br />

/// Gehört der Klasse an


144<br />

Kommentarbasierte Dokumentation in XML<br />

Dieser Kommentar enthält somit einen Verweis auf die Beschreibung der Klasse<br />

RequestWebPage. Beachten Sie, dass die Tags der XML-Syntax unterworfen sind,<br />

also die Unterscheidung zwischen Groß- und Kleinschreibung eine Rolle spielt.<br />

Ein anderes interessantes Tag für Dokumentation von Elementen ist das -Tag.<br />

Es gestattet Ihnen, Querverweise auf andere Themen einzufügen, die<br />

für den Leser interessant sein könnten:<br />

/// <br />

Der Querverweis in diesem Beispiel ermöglicht es dem Benutzer, in die Dokumentation<br />

für den Namensraum System.Net zu verzweigen. Sofern Sie auf ein<br />

Element verweisen, das im aktuellen Namensraum nicht sichtbar ist, müssen Sie<br />

den vollständig qualifizierten Bezeichner angegeben.<br />

Das folgende Listing demonstriert wie angekündigt am Beispiel der Klasse<br />

RequestWebPage, wie die professionelle Dokumentation von Elementen konkret<br />

aussehen kann. Beachten Sie, wie die einzelnen XML-Tags angeordnet und verschachtelt<br />

sind:<br />

Listing 9.5:<br />

Dokumentation von Elementen mit den Tags , , <br />

und <br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: /// Klasse fordert eine Webseite von einem Webserver an<br />

<br />

7: public class RequestWebPage<br />

8: {<br />

9: private const int BUFFER_SIZE = 128;<br />

10:<br />

11: /// m_strURL speichert den URL der Webseite<br />

12: private string m_strURL;<br />

13:<br />

14: /// RequestWebPage() ist der Konstruktor der Klasse<br />

15: /// für Aufruf ohne Argumente.<br />

<br />

16: public RequestWebPage()<br />

17: {<br />

18: }<br />

19:<br />

20: /// RequestWebPage(string strURL) ist der Konstruktor der<br />

Klasse<br />

21: /// bei Initialisierung mit URL.<br />


Kapitel 9 • Konfiguration und Verbreitung 145<br />

22: public RequestWebPage(string strURL)<br />

23: {<br />

24: m_strURL = strURL;<br />

25: }<br />

26:<br />

27: public string URL<br />

28: {<br />

29: get { return m_strURL; }<br />

30: set { m_strURL = value; }<br />

31: }<br />

32:<br />

33: /// Methode GetContent(out string strContent):<br />

34: /// Ist Element der Klasse <br />

<br />

35: /// Verwendet die Variable <br />

36: /// Fordert den Inhalt einer Webseite an. <br />

37: /// Setzt voraus, dass URL (http://...) für Webseite<br />

zuvor in<br />

38: /// der privaten Elementvariable m_strURL gespeichert wurde.<br />

Diese<br />

39: /// Initialisierung kann bei Konstruktion des Objekts<br />

geschehen,<br />

40: /// aber auch durch Setzen der Eigenschaft .<br />

<br />

41: /// <br />

42: /// <br />

43: /// <br />

44: /// <br />

45: /// <br />

46: /// <br />

47: /// <br />

48: /// <br />

49:<br />

50: public bool GetContent(out string strContent)<br />

51: {<br />

52: strContent = "";<br />

53: // ...<br />

54: return true;<br />

55: }<br />

56: }<br />

9.2.2 Bemerkungen und Auflistungen einfügen<br />

Das Tag sollte nur die essenzielle Information zu einem Element<br />

zusammenfassen. Für den ausführlichen Teil der Dokumentation, sonstige Prosa,<br />

Hinweise und Bemerkungen steht das Tag zur Verfügung.


146<br />

Kommentarbasierte Dokumentation in XML<br />

Ferner sind Sie bei der Absatzformatierung nicht allein auf das -Tag<br />

beschränkt. So können Sie in einem -Abschnitt beispielsweise auch<br />

Auflistungen (mit Auflistungszeichen oder auch nummeriert) nach folgendem<br />

Muster einfügen<br />

/// <br />

/// Konstruktoren<br />

/// und<br />

/// <br />

/// <br />

/// <br />

Beachten Sie, dass diese Auflistung aus nur einem Element besteht, aber zwei<br />

Querverweise enthält – nämlich auf die Beschreibungen der beiden Konstruktoren<br />

der Klasse. Natürlich kann eine Auflistung beliebig viele Elemente enthalten und<br />

auch die Inhalte der einzelnen Auflistungselemente lassen sich nach Belieben<br />

gestalten.<br />

Ein weiteres Tag, das sich ganz gut in -Abschnitten macht, ist .<br />

Sie verwenden es, um im Rahmen einer Beschreibung auf die Deklaration<br />

eines Parameters zu verweisen. Die Dokumentation für den Parameter des zweiten<br />

Konstruktors könnte beispielsweise so aussehen:<br />

/// Überträgt den im Parameter <br />

/// übergebenen URL in Elementvariable .<br />

/// <br />

public RequestWebPage(string strURL)<br />

Das folgende Listing zeigt all diese Tags – zusammen mit den bereits vorgestellten<br />

– in Aktion:<br />

Listing 9.6:<br />

Bemerkungen und Auflistungen in die Dokumentation einfügen<br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: /// Klasse fordert eine Webseite von einem Webserver an<br />

<br />

7: /// Die Klasse RequestWebPage hat die Elemente:<br />

8: /// Methoden:<br />

9: /// <br />

10: /// Konstruktoren<br />

11: /// und<br />

12: /// <br />

13: /// <br />

14: ///


Kapitel 9 • Konfiguration und Verbreitung 147<br />

15: /// <br />

16: /// Eigenschaften:<br />

17: /// <br />

18: /// <br />

19: /// <br />

20: /// <br />

21: /// <br />

22: /// <br />

23: /// <br />

24: public class RequestWebPage<br />

25: {<br />

26: private const int BUFFER_SIZE = 128;<br />

27:<br />

28: /// m_strURL speichert den URL der Webseite<br />

29: private string m_strURL;<br />

30:<br />

31: /// RequestWebPage() ist der Konstruktor der Klasse<br />

32: /// für Aufruf ohne Argumente.<br />

<br />

33: public RequestWebPage()<br />

34: {<br />

35: }<br />

36:<br />

37: /// RequestWebPage(string strURL) ist der Konstruktor der<br />

Klasse<br />

38: /// bei Initialisierung mit URL.<br />

<br />

39: /// Überträgt den im Parameter <br />

40: /// übergebenen URL in Elementvariable .<br />

<br />

41: public RequestWebPage(string strURL)<br />

42: {<br />

43: m_strURL = strURL;<br />

44: }<br />

45:<br />

46: /// Setzt den Wert von .<br />

47: /// Liefert den Wert von .<br />

48: public string URL<br />

49: {<br />

50: get { return m_strURL; }<br />

51: set { m_strURL = value; }<br />

52: }<br />

53:<br />

54: /// Methode GetContent(out string strContent):<br />

55: /// Ist Element der Klasse <br />

<br />

56: /// Verwendet die Variable


148<br />

Kommentarbasierte Dokumentation in XML<br />

57: /// Fordert den Inhalt einer Webseite an. <br />

58: /// Setzt voraus, dass URL (http://...) für Webseite<br />

zuvor in<br />

59: /// der privaten Elementvariable m_strURL gespeichert wurde.<br />

Diese<br />

60: /// Initialisierung kann bei Konstruktion des Objekts geschehen,<br />

61: /// aber auch durch Setzen der Eigenschaft .<br />

<br />

62: /// <br />

63: /// Fordert den Inhalt der in der Eigenschaft<br />

<br />

64: /// spezifizierten Webseite an und gibt diesen im Ausgabeparameter<br />

65: /// zurück.<br />

66: /// Die Implementation der Methode benutzt die Elemente:<br />

67: /// <br />

68: /// .<br />

<br />

69: /// <br />

70: /// <br />

<br />

71: /// <br />

72: /// <br />

73: /// und dessen<br />

Methode<br />

74: /// <br />

75: /// für das Objekt<br />

76: /// .<br />

77: /// <br />

78: /// <br />

79: /// <br />

80: public bool GetContent(out string strContent)<br />

81: {<br />

82: strContent = "";<br />

83: // ...<br />

84: return true;<br />

85: }<br />

86: }<br />

9.2.3 Beispiele anführen<br />

Es gibt keinen besseren Weg, den Gebrauch eines Objekts oder einer Methode zu<br />

dokumentieren, als über prototypisches Beispiel. Somit dürfte es nicht verwundern,<br />

dass Sie für die Dokumentation auch die Tags und ver-


Kapitel 9 • Konfiguration und Verbreitung 149<br />

wenden können. Das -Tag umschließt das gesamte Beispiel – also<br />

Beschreibung und Code –, das -Tag (welch Überraschung) nur den Code<br />

des Beispiels.<br />

Das folgende Listing demonstriert anhand der Dokumentation für die Konstruktoren,<br />

wie man Codebeispiele in die Dokumentation einfügt. Das Beispiel für den<br />

Aufruf der Methode GetContent können Sie dann selbst ergänzen.<br />

Listing 9.7:<br />

Konzepte mit Beispielcode dokumentieren<br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: /// Klasse fordert eine Webseite von einem Webserver an<br />

<br />

7: /// ... <br />

8: public class RequestWebPage<br />

9: {<br />

10: private const int BUFFER_SIZE = 128;<br />

11:<br />

12: /// m_strURL speichert den URL der Webseite<br />

13: private string m_strURL;<br />

14:<br />

15: /// RequestWebPage() ist der Konstruktor ... <br />

16: /// Dieses Beispiel demonstriert den Aufruf des parameterlosen<br />

17: /// Konstruktors der Klasse:<br />

18: /// <br />

19: /// public class MyClass<br />

20: /// {<br />

21: /// public static void Main()<br />

22: /// {<br />

23: /// public<br />

24: /// string strContent;<br />

25: /// RequestWebPage objRWP = new RequestWebPage();<br />

26: /// objRWP.URL = "http://www.alphasierrapapa.com";<br />

27: /// objRWP.GetContent(out strContent);<br />

28: /// Console.WriteLine(strContent);<br />

29: /// }<br />

30: /// }<br />

31: /// <br />

32: /// <br />

33: public RequestWebPage()<br />

34: {<br />

35: }<br />

36:


150<br />

Kommentarbasierte Dokumentation in XML<br />

37: /// RequestWebPage(string strURL) ist ... <br />

38: /// ... <br />

39: /// Dieses Beispiel zeigt den Aufruf des zweiten Konstruktors<br />

40: /// der Klasse, der gleichzeitig die Eigenschaft URL initialisiert:<br />

41: /// <br />

42: /// public class MyClass<br />

43: /// {<br />

44: /// public static void Main()<br />

45: /// {<br />

46: /// string strContent;<br />

47: /// RequestWebPage objRWP = new<br />

/// RequestWebPage("http://www.alphasierrapapa.com");<br />

48: /// objRWP.GetContent(out strContent);<br />

49: /// Console.WriteLine("\n\nInhalt der Webseite "+ objRWP.<br />

URL+":\n\n");<br />

50: /// Console.WriteLine(strContent);<br />

51: /// }<br />

52: /// }<br />

53: /// <br />

54: /// <br />

55: public RequestWebPage(string strURL)<br />

56: {<br />

57: m_strURL = strURL;<br />

58: }<br />

59:<br />

60: /// ... <br />

61: public string URL<br />

62: {<br />

63: get { return m_strURL; }<br />

64: set { m_strURL = value; }<br />

65: }<br />

66:<br />

67: /// Methode GetContent(out string strContent): ...<br />

<br />

68: /// ... <br />

69: /// <br />

70: public bool GetContent(out string strContent)<br />

71: {<br />

72: strContent = "";<br />

73: // ...<br />

74: return true;<br />

75: }<br />

76: }


Kapitel 9 • Konfiguration und Verbreitung 151<br />

9.2.4 Parameter beschreiben<br />

Eine weitere sehr wichtige Aufgabe der Dokumentation, die bisher noch keine<br />

Erwähnung fand, ist die Beschreibung der Parameter von Konstruktoren und<br />

Methoden. Die Umsetzung ist geradlinig. Sie verwenden einfach das -Tag<br />

nach folgendem Muster:<br />

/// <br />

/// Wird dazu verwendet, die Eigenschaft URL gleich bei Konstruktion<br />

/// eines Objekts auf den URL der Webseite zu setzen. Der Wert wird in<br />

/// der Elementvariable gespeichert.<br />

Das war die Beschreibung für einen einfachen Eingabeparameter. Beachten Sie,<br />

dass einem -Tag auch -Tags untergeordnet sein können.<br />

Ein Rückgabeparameter wird etwas anders beschrieben.<br />

/// <br />

/// true: Webseite erfolgreich gelesen. <br />

/// false: Webseite nicht gelesen. <br />

/// <br />

Wie Sie sehen, geschieht die Beschreibung von Rückgabeparametern mit<br />

-Tags. Das vollständige Beispiel zeigt Listing 9.8<br />

Listing 9.8:<br />

Parameter von Methoden und Ergebniswerte beschreiben<br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: /// Klasse fordert eine Webseite von einem Webserver an<br />

<br />

7: /// ... <br />

8: public class RequestWebPage<br />

9: {<br />

10: private const int BUFFER_SIZE = 128;<br />

11:<br />

12: /// m_strURL speichert den URL der Webseite<br />

13: private string m_strURL;<br />

14:<br />

15: /// RequestWebPage() ist der Konstruktor ... <br />

16: /// Dieses Beispiel demonstriert ...<br />

17: /// <br />

18: /// public class MyClass<br />

19: /// {<br />

20: /// ...


152<br />

Kommentarbasierte Dokumentation in XML<br />

21: /// }<br />

22: /// <br />

23: /// <br />

24: public RequestWebPage()<br />

25: {<br />

26: }<br />

27:<br />

28: /// RequestWebPage(string strURL) ist ... <br />

29: /// ... <br />

30: /// <br />

31: /// Wird dazu verwendet, die Eigenschaft URL gleich bei Konstruktion<br />

32: /// eines Objekts auf den URL der Webseite zu setzen. Der Wert<br />

wird in<br />

33: /// der Elementvariable gespeichert.<br />

<br />

34: /// ... <br />

35: public RequestWebPage(string strURL)<br />

36: {<br />

37: m_strURL = strURL;<br />

38: }<br />

39:<br />

40: /// ... <br />

41: public string URL<br />

42: {<br />

43: get { return m_strURL; }<br />

44: set { m_strURL = value; }<br />

45: }<br />

46:<br />

47: /// Methode GetContent(out string strContent): ...<br />

<br />

48: /// Fordert den Inhalt der in der Eigenschaft<br />

<br />

49: /// spezifizierten Webseite an und gibt diesen im Ausgabeparameter<br />

50: /// zurück.<br />

51: /// Die Implementation der Methode benutzt die Elemente: ...<br />

52: /// <br />

53: /// Enthält den Inhalt der Webseite.<br />

<br />

54: /// <br />

55: /// true: Webseite erfolgreich gelesen. <br />

56: /// false: Webseite nicht gelesen. <br />

57: /// <br />

58: /// <br />

59: public bool GetContent(out string strContent)<br />

60: {


Kapitel 9 • Konfiguration und Verbreitung 153<br />

61: strContent = "";<br />

62: // ...<br />

63: return true;<br />

64: }<br />

65: }<br />

9.2.5 Eigenschaften beschreiben<br />

Zur Beschreibung der Eigenschaften einer Klasse benutzen Sie anstelle des<br />

-Tags das speziell für diesen Elementtyp zuständige -Tag.<br />

Listing 9.9 demonstriert den Einsatz dieses Tags für die Eigenschaft URL der<br />

Klasse RequestWebPage (ab Zeile 30). Darüber hinaus gibt das Listing nun auch<br />

einen guten Überblick über die gesamte Struktur der kommentarbasierten Dokumentation<br />

für die Komponente RequestWebPage (auch wenn die einzelnen Teile<br />

aus Platzgründen fehlen):<br />

Listing 9.9:<br />

Eigenschaften mit dem -Tag dokumentieren<br />

1: using System;<br />

2: using System.Net;<br />

3: using System.IO;<br />

4: using System.Text;<br />

5:<br />

6: /// Klasse fordert eine Webseite von einem Webserver an<br />

<br />

7: /// ... <br />

8: public class RequestWebPage<br />

9: {<br />

10: private const int BUFFER_SIZE = 128;<br />

11:<br />

12: /// m_strURL speichert den URL der Webseite<br />

13: private string m_strURL;<br />

14:<br />

15: /// RequestWebPage() ist der Konstruktor ... <br />

16: /// ... <br />

17: public RequestWebPage()<br />

18: {<br />

19: }<br />

20:<br />

21: /// RequestWebPage(string strURL) ist ... <br />

22: /// ... <br />

23: /// ... <br />

24: /// Dieses Beispiel ... <br />

25: public RequestWebPage(string strURL)<br />

26: {<br />

27: m_strURL = strURL;<br />

28: }


154<br />

Kommentarbasierte Dokumentation in XML<br />

29:<br />

30: /// Die Eigenschaft URL liefert/setzt den URL der Webseite<br />

31: /// Setzt den Wert von .<br />

32: /// Liefert den Wert von .<br />

33: public string URL<br />

34: {<br />

35: get { return m_strURL; }<br />

36: set { m_strURL = value; }<br />

37: }<br />

38:<br />

39: /// Methode GetContent(out string strContent): ...<br />

<br />

40: /// Fordert den Inhalt der in der Eigenschaft<br />

<br />

41: /// spezifizierten Webseite an und gibt diesen im Ausgabeparameter<br />

42: /// zurück.<br />

43: /// Die Implementation der Methode benutzt die Elemente: ...<br />

44: /// <br />

45: /// Enthält den Inhalt der Webseite.<br />

<br />

46: /// <br />

47: /// true: Webseite erfolgreich gelesen. <br />

48: /// false: Webseite nicht gelesen. <br />

49: /// <br />

50: /// <br />

51: public bool GetContent(out string strContent)<br />

52: {<br />

53: strContent = "";<br />

54: // ...<br />

55: return true;<br />

56: }<br />

57: }<br />

9.2.6 Kompilieren der Dokumentation<br />

Die Dokumentation Ihrer Komponente umfasst nun eine ausführliche Erläuterung<br />

aller Konstruktoren, Methoden und deren Parameter sowie der Eigenschaften,<br />

Elementvariablen usw. Damit wäre sie vollständig und kann in eine XML-Datei<br />

kompiliert werden, welche Sie schließlich Ihren Kunden als Informationsquelle<br />

über das Innenleben Ihrer Komponente überlassen können. Damit der Compiler<br />

im Rahmen eines gewöhnlichen Übersetzungslaufs die XML-Datei aus den entsprechenden<br />

Kommentaren im Quellcode zusammenstellt, ist nichts weiter als die<br />

zusätzliche Angabe des Schalters /doc: zusammen mit dem Namen der zu erstellenden<br />

XML-Datei erforderlich.<br />

csc /r:System.Net.dll /doc:wrq.xml /t:library /out:wrq.dll requestwebpage.cs


Kapitel 9 • Konfiguration und Verbreitung 155<br />

Vorausgesetzt, die Dokumentation enthält keine Fehler (ja, sie wird auf ihre Gültigkeit<br />

hin überprüft), generiert der Compiler bei diesem Übersetzungslauf unter<br />

anderem die Datei wrq.xml, in der schließlich die gesamte Dokumentation Ihrer<br />

Komponente enthalten ist.<br />

Sie können sich das Ergebnis nun zwar wiederum als Listing ansehen, interessanter<br />

dürfte es aber sein, wenn Sie die Datei im Internet Explorer betrachten. Wie in<br />

Abbildung 9.1 gezeigt, gibt der Internet Explorer die hierarchische Struktur der<br />

im Quelltext befindlichen Dokumentation wieder und gestattet es auch, sie interaktiv<br />

zu durchforsten.<br />

Bild 9.1:<br />

Betrachten der ins XML-Format übersetzten Dokumentation im Internet<br />

Explorer<br />

Ohne dass es viel Sinn machen würde, an dieser Stelle tiefer in die Semantik der<br />

XML-Datei einzusteigen, sollten Sie aber dennoch wissen, wie das name-Attribut<br />

eines member-Tags zu lesen ist. Der erste Teil des Attributs, der Buchstabe vor dem<br />

Doppelpunkt, gibt Auskunft über die Art des Elements, der zweite ist der im<br />

Quellcode verwendete Bezeichner:<br />

• N – Bezeichner steht für einen Namensraum<br />

• T – Bezeichner steht für einen Datentyp (Klasse, Schnittstelle, Strukturtyp,<br />

Aufzählungstyp oder Delegation)


156<br />

Versionspflege<br />

• F – Bezeichner steht für eine Elementvariable einer Klasse<br />

• P – Bezeichner steht für eine Eigenschaft (kann auch Indizierer oder indizierte<br />

Eigenschaft sein)<br />

• M – Bezeichner steht für eine Methode (Operatoren, Konstruktoren, Destrukturen<br />

inbegriffen)<br />

• E – Bezeichner steht für ein Ereignis<br />

• ! – Bezeichner steht für eine Fehlermeldung über einen Verweis, den der C#-<br />

Compiler nicht auflösen konnte<br />

Alle Bezeichner sind vollständig qualifiziert, das heißt, sie enthalten alle Namensanteile<br />

und Typzusätze bis hin zur Wurzel des Namensraums. Der Konstruktor ist<br />

durch #ctor und der Destruktor durch Finalize notiert. Die Parameter einer<br />

Methode folgen in Klammern auf den Bezeichner, wie üblich durch Kommas<br />

getrennt. Jeder einzelne Parameter ist als reine Typspezifikation notiert (NGWS-<br />

Signatur, wenn der Typ dem NGWS-Subsystem entstammt). Die Typspezifikation<br />

eines Ausgabeparameters trägt das Suffix @.<br />

Unter normalen Umständen müssen Sie sich jedoch nicht weiter um die Details<br />

der XML-Dokumentation kümmern. Erzeugen Sie einfach die Datei und packen<br />

Sie sie Ihrer Komponente bei. Wer Programmierwerkzeuge benutzt, wird dann<br />

zweifelsohne seine Freude mit Ihrer Software haben.<br />

9.3 Versionspflege<br />

Die Versionspflege gerät inzwischen mehr und mehr zum Balanceakt über einer<br />

sich ständig ändernden Landschaft von DLLs. Einzelne Anwendungen installieren<br />

immer wieder neue Versionen gemeinsam genutzter Komponenten, bis plötzlich<br />

die eine oder andere Anwendung ihren Dienst versagt, weil sie mit der aktuell<br />

installierten Version einer Komponente nicht mehr zurechtkommt. In der Tat<br />

bereiten gemeinsam genutzte Komponenten heutzutage mehr Probleme als sie<br />

lösen.<br />

Eines der primären Ziele der NGWS-Laufzeitumgebung ist die Beseitigung leidiger<br />

Versionskonflikte. Zentrales Moment in dem Lösungsansatz ist das Konzept<br />

der NGWS-Komponenten (wobei hier die Verpackung und nicht der Inhalt im<br />

Vordergrund steht), das es einem Entwickler unter anderem ermöglicht, Regeln<br />

für die Versionsabhängigkeiten zwischen den verschiedenen Teilen seiner Software<br />

zu spezifizieren – wobei das NGWS-Subsystem diese Regeln zur Laufzeit<br />

durchsetzt.<br />

Dazu erst einmal ein wenig mehr über NGWS-Komponenten, damit Sie eine Vorstellung<br />

davon erhalten, wofür man sie einsetzt und wie sie sich – mit Blick auf<br />

die Versionspflege – von DLLs unterscheiden.


Kapitel 9 • Konfiguration und Verbreitung 157<br />

9.3.1 NGWS-Komponenten<br />

Obwohl es an der entsprechenden Stelle in Kapitel 8 nicht eigens erwähnt ist: Die<br />

erste Bibliothek, die Sie kompiliert haben, war nichts anderes als eine NGWS-<br />

Komponente, aus dem schlichten Grund, weil der C#-Compiler Komponenten<br />

grundsätzlich als NGWS-Komponenten übersetzt. Was also ist eine NGWS-Komponente?<br />

Die NGWS-Komponente ist in erster Linie die grundlegende Einheit für die<br />

gemeinsame Nutzung von Code im Rahmen der NGWS-Laufzeitumgebung. Wie<br />

nicht anderes zu erwarten, bildet sie auch die Grenze für die Versionskontrolle,<br />

die Sicherheitsprüfung, die Klassengliederung und die Typauflösung. C#-Anwendungen<br />

setzen sich somit typischerweise aus mehreren NGWS-Komponenten<br />

zusammen.<br />

Nachdem es in diesem Abschnitt um die Versionsverwaltung geht, stellt sich die<br />

Frage »Wie ist die Versionsinformation überhaupt beschaffen?«. Die Versionsnummer<br />

einer NGWS-Komponente setzt sich allgemein aus vier Teilen zusammen:<br />

MajorVersion.MinorVersion.BuildNumber.Revision<br />

Diese Versionsnummer heißt Kompatibiliätsversion und entscheidet darüber, welche<br />

Version einer NGWS-Komponente die Laderoutine des Laufzeitsystems bei<br />

Anforderung der Komponente auswählt. Eine Version wird als inkompatibel eingestuft,<br />

wenn sie in den Bestandteilen MajorVersion und MinorVersion nicht<br />

mit der angeforderten Version übereinstimmt. Falls dagegen der Bestandteil<br />

BuildNumber abweicht, gilt die Komponente noch als kompatibel. Vollständige<br />

Kompatibilität liegt vor, wenn Abweichungen auf den Bestandteil Revision<br />

beschränkt bleiben – unterschiedliche Revisionsnummern resultieren aus dem<br />

sogenannten QuickFix-Engineering, das sich auf die reine Fehlerbeseitigung<br />

beschränkt.<br />

Darüber hinaus enthält eine NGWS-Komponente noch eine zweite Versionsinformation,<br />

die sogenannte informale Version. Wie der Name bereits vermuten lässt,<br />

dient diese Information – die etwa nach dem Muster MeinSuperControl Build<br />

1890 gestrickt ist – nur dem Zweck der Dokumentation und bleibt für die maschinelle<br />

Versionsprüfung ohne Bedeutung.<br />

Bevor es in den folgenden beiden Abschnitten um private und gemeinsam<br />

genutzte NGWS-Komponenten geht, sollten Sie noch den Compilerschalter<br />

/a.version zum Setzen der Kompatibilitätsversion kennenlernen. Wenn Sie der<br />

Komponente RequestWebpage beispielsweise die Versionsinformation 1.0.1.0<br />

zuordnen wollen, lautet der Compileraufruf:<br />

csc /a.version:1.0.1.0 /t:library /out:wrq.dll requestwebpage.cs


158<br />

Versionspflege<br />

Sie überprüfen die Versionsnummer, indem Sie den Eigenschaften-Dialog der<br />

resultierenden Datei wrq.dll über ein Explorer-Fenster öffnen und die Registerkarte<br />

Version anzeigen.<br />

Private NGWS-Komponenten<br />

Wenn Sie eine Anwendung (durch Angabe des Compilerschalters /reference:)<br />

mit einer Komponente verknüpfen, hinterlegt der Compiler im Ladeverzeichnis<br />

der ausführbaren Datei bzw. Bibliothek eine Abhängigkeitsinformation, die insbesondere<br />

auch die Versionsnummern der verknüpften Bibliotheken umfasst. Diese<br />

Abhängigkeitsinformation wird beim Start der Anwendung ausgewertet, um dann<br />

zur Laufzeit die richtige Version der NGWS-Komponente bereitstellen zu können.<br />

Falls Sie nun der Meinung sind, dass auch die als Beispiel diskutierte NGWS-<br />

Komponente wrq.dll der Versionsprüfung unterliegt, befinden Sie sich im Irrtum:<br />

NGWS-Komponenten, die im Ausführungspfad der Anwendung zu finden sind,<br />

betrachtet das Laufzeitsystem als private Komponenten und unterwirft sie daher<br />

keiner Versionsprüfung. Der Grund dafür ist einleuchtend: Was Sie zusammen<br />

mit der ausführbaren Datei Ihrer Anwendung in ein Verzeichnis packen, bleibt<br />

ebenso Ihnen überlassen, wie die Sicherstellung der Kompatibilität unter den<br />

gelieferten Dateien.<br />

Nachteile sind bei der Verwendung privater NGWS-Komponenten keine zu<br />

erwarten. Denn selbst wenn eine andere Anwendung die gleiche Komponente in<br />

einer anderen Version für die gemeinsame Nutzung installieren sollte, wird Ihre<br />

Anwendung von dieser keinen Gebrauch machen, da sie unter Angabe einer privaten<br />

NGWS-Komponente übersetzt wurde. Und den zusätzlichen Speicherplatzbedarf<br />

dürften die ausbleibenden Versionskonflikte mehr als aufwiegen.<br />

Gemeinsam genutzte NGWS-Komponenten<br />

Wenn Sie Software für die gemeinsame Nutzung durch mehrere Anwendungen<br />

schreiben wollen, bieten sich gemeinsam genutzte NGWS-Komponenten an.<br />

Allerdings müssen Sie dazu eine Reihe zusätzlicher Vorkehrungen treffen.<br />

Zunächst einmal müssen Sie sich einen eindeutigen Namen für die Komponente<br />

aussuchen. Vielleicht werden Sie sich schon gefragt haben, wann endlich das<br />

Gegenstück der aus dem COM bekannten GUID (global einzigartiger Bezeichner)<br />

ins Spiel kommt – hier ist es. Solange Sie mit privaten NGWS-Komponenten<br />

hantieren, sind keine global eindeutigen Bezeichner erforderlich; erst wenn Sie<br />

eine NGWS-Komponente für die gemeinsame Nutzung installieren wollen, benötigen<br />

Sie einen eindeutigen Namen.<br />

Die Eindeutigkeit des Namens wird durch ein kryptografisches Standardverfahren<br />

auf Basis eines öffentlichen Schlüssels garantiert: Sie als Entwickler signieren<br />

Ihre NGWS-Komponente mit einem privaten Schlüssel, während die Anwendung


Kapitel 9 • Konfiguration und Verbreitung 159<br />

einen öffentlichen Schlüssel benutzt, um die Authentizität des Urhebers der<br />

NGWS-Komponente zu prüfen. Nachdem Sie Ihre NGWS-Komponente signiert<br />

haben, können Sie sie in den allgemeinen Cache für NGWS-Komponenten oder<br />

in das Verzeichnis Ihrer Anwendung kopieren. Alles Weitere erledigt das Laufzeitsystem:<br />

Es kümmert sich darum, dass die Komponente fortan allen Anwendungen<br />

zur Verfügung steht.<br />

Zahlt sich die gemeinsame Nutzung von NGWS-Komponenten denn überhaupt<br />

aus? Nach Einschätzung des Autors eher nicht Vielmehr setzen Sie Ihren Code<br />

erneut potenziellen Versionskonflikten aus – obwohl sich Konflikte natürlich<br />

weitgehend dadurch ausschalten lassen, dass andere Entwickler, die Ihre NGWS-<br />

Komponente einsetzen, geeignete Bindungsregeln verwenden. Da der Speicherplatz,<br />

den eine Komponente benötigt, heutzutage wohl nicht mehr sonderlich ins<br />

Gewicht fällt, sei Ihnen weitgehend die Verwendung privater NGWS-Komponenten<br />

empfohlen, denen Sie ja trotzdem eindeutige Namen geben können.<br />

9.4 Zusammenfassung<br />

Dieses Kapitel hat Ihnen drei Techniken vorgestellt, die Sie Ihren Komponenten<br />

und Anwendungen vor der Verbreitung angedeihen lassen können. Die erste<br />

Technik ist die bedingte Übersetzung: Der Einsatz des (vom Compiler emulierten)<br />

C#-Präprozessors sowie des conditional-Attributs ermöglichen es Ihnen,<br />

verschiedene Bestandteile des Codes in Abhängigkeit von der Definition einzelner<br />

oder mehrerer Symbole ein- und auszublenden. Auf diese Weise können Sie<br />

denselben Quelltext verwenden, um unterschiedliche Versionen (beispielsweise<br />

Debug-, Demo- und Release-Version) zu kompilieren.<br />

Die zweite Technik ist die kommentarbasierte Dokumentation. Da die korrekte<br />

und ausführliche Dokumentation heutzutage als wichtiger Bestandteil des Entwicklungsvorgangs<br />

gilt und nicht mehr nur als leidiges Nachspiel, sollte man auf<br />

die mit C# verfügbare automatisierte Erstellung einer Dokumentation keinesfalls<br />

verzichten, zumal das vom Compiler generierte XML-Format eine direkte Integration<br />

der Informationen in das Hilfesystem anderer Programmierwerkzeuge,<br />

beispielsweise Visual Studio 7, ermöglicht.<br />

Die dritte Technik betrifft die Versionspflege von NGWS-Komponenten, der<br />

kleinsten eigenständigen Einheit des NGWS-Subsystems. Prinzipiell haben Sie<br />

die Wahl zwischen privaten und gemeinsam genutzten NGWS-Komponenten.<br />

Obwohl das NGWS-Laufzeitsystem für gemeinsam genutzte Komponenten eine<br />

Versionsprüfung zur Ladezeit vorsieht, beugt eine Beschränkung auf private<br />

Komponenten Versionskonflikten immer noch am einfachsten und wirksamsten<br />

vor.


Kapitel 10<br />

Integration von nicht verwaltetem<br />

Code<br />

10.1 Zusammenarbeit mit COM 162<br />

10.2 Aufruf plattformspezifischer Dienste 174<br />

10.3 Unsicherer Code 176<br />

10.4 Zusammenfassung 177


162<br />

Zusammenarbeit mit COM<br />

Das NGWS-Laufzeitsystem ist fürwahr ein Triumph der Technik – und wäre keinen<br />

rostigen Heller wert, wenn es sich gegen die Wiederverwendung existierenden<br />

nicht verwalteten Codes sperren würde; unabhängig davon, ob es dabei um<br />

COM-Komponenten oder um exportierte Funktionen aus C-DLLs geht. Außerdem<br />

mag es Situationen geben, in denen verwalteter Code der Performance im<br />

Wege steht.<br />

NGWS und C# bieten Ihnen die folgenden Möglichkeiten zur Integration nicht<br />

verwalteten Codes:<br />

• Zusammenarbeit mit COM<br />

• Aufruf plattformspezifischer Dienste<br />

• unsicherer Code<br />

10.1 Zusammenarbeit mit COM<br />

Da COM und NGWS wohl für längere Zeit nebeneinander existieren werden, ist<br />

die Interoperabilität mit COM wohl das wichtigste Teilgebiet, wenn es um nicht<br />

verwalteten Code geht: Clients des NGWS-Laufzeitsystems müssen in der Lage<br />

sein, existierende COM-Komponenten zu verwenden, und COM-Clients muss die<br />

Möglichkeit offen stehen, sich der neuen NGWS-Komponenten zu bedienen.<br />

Die folgenden Abschnitte setzen sich mit beiden Wegen auseinander:<br />

• Verfügbarkeit von NGWS-Objekten für COM<br />

• Verfügbarkeit von COM-Objekten für NGWS-Anwendungen<br />

Auch wenn sich die Diskussion auf C# konzentriert, behalten Sie bitte im Kopf,<br />

dass diese Techniken und Prinzipien in gleicher Weise auf VB sowie auf verwaltetes<br />

C++ anwendbar sind. Tatsächlich geht es hier um die Fähigkeiten des<br />

NGWS-Laufzeitsystems und nicht um die Möglichkeiten einer einzelnen Programmiersprache.<br />

10.1.1 Verfügbarkeit von NGWS-Objekten für COM<br />

Die eine Richtung in der Zusammenarbeit zwischen den beiden Systemen besteht<br />

darin, COM-Clients die Benutzung von Komponenten des NGWS-Laufzeitsystems<br />

zu erlauben (die, wie gesagt, in einer der Programmiersprachen C#, VB oder<br />

verwaltetem C++ geschrieben sein können). Die folgenden Abschnitte demonstrieren,<br />

wie das in der Praxis aussieht, und verwenden als Beispiel die in Kapitel 8,<br />

»Komponenten mit C# schreiben«, erstellten Komponenten RequestWebPage und<br />

WhoisLookup (beide in der Version mit eigenem Namensraum).


Kapitel 10 • Integration von nicht verwaltetem Code 163<br />

Im Wesentlichen geht es dabei um zwei Punkte:<br />

• Registrieren eines Objekts beim NGWS-Laufzeitsystem<br />

• Aufruf eines NGWS-Objekts durch einen COM-Client<br />

Registrieren eines Objekts beim NGWS-Laufzeitsystem<br />

COM-Objekte müssen registriert (also in der Registrierung des Systems eingetragen)<br />

werden, bevor man sie verwenden kann. Für COM 1.0 geschieht das üblicherweise<br />

über das Windows-Utility RegSvr32, das sich für COM+ (also die<br />

COM-Version 2.0 und damit die NGWS) nicht verwenden lässt. Hierfür gibt es<br />

ein eigenes Hilfsprogramm namens RegAsm.Exe.<br />

RegAsm trägt NGWS-Komponenten in die Registrierung des Systems ein und<br />

berücksichtigt dabei sämtliche in einer Komponente enthaltenen Klassen, soweit<br />

sie öffentlich verfügbar sind. Außerdem erzeugt dieses Programm optional eine<br />

Registrierungsdatei (.REG), die sich nicht nur bei der Installation auf anderen<br />

Systemen nützlich macht, sondern auch die schnelle Prüfung ermöglicht, welche<br />

Einträge der Registrierung hinzugefügt wurden.<br />

Der Aufruf für die in Kapitel 8 erstellte Bibliothek sieht so aus:<br />

regasm presenting.csharp.dll /reg:presenting.csharp.reg<br />

Listing 10.1 gibt die dabei erzeugte Registrierungsdatei (csharp.reg) wieder. Wer<br />

Erfahrung mit COM hat, dem dürfte die Art der meisten Einträge geläufig sein.<br />

Die ID der Komponenten wird hier aus dem Namensraum und dem Klassennamen<br />

generiert.<br />

Listing 10.1: Die von regasm.exe erzeugte Registrierungsdatei<br />

1: REGEDIT4<br />

2:<br />

3: [HKEY_CLASS_ROOT\Presenting.CSharp.RequestWebPage]<br />

4: @="COM+ class: Presenting.CSharp.RequestWebPage"<br />

5:<br />

6: [HKEY_CLASS_ROOT\Presenting.CSharp.RequestWebPage\CLSID]<br />

7: @="{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}"<br />

8:<br />

9: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}]<br />

10: @="COM+ class: Presenting.CSharp.RequestWebPage"<br />

11:<br />

12: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}<br />

\InprocServer32]<br />

13: @="D:\WINNT\System32\MSCorEE.dll"<br />

14: "ThreadingModel"="Both"<br />

15: "Class"="Presenting.CSharp.RequestWebPage"<br />

16: "Assembly"="csharp, Ver=1.0.1.0"


164<br />

Zusammenarbeit mit COM<br />

17:<br />

18: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-_å58F9F5ADEEA3}<br />

\ProgId]<br />

19: @="Presenting.CSharp.RequestWebPage"<br />

20:<br />

21: [HKEY_CLASS_ROOT\Presenting.CSharp.WhoisLookup]<br />

22: @="COM+ class: Presenting.CSharp.WhoisLookup"<br />

23:<br />

24: [HKEY_CLASS_ROOT\Presenting.CSharp.WhoisLookup\CLSID]<br />

25: @="{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}"<br />

26:<br />

27: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}]<br />

28: @="COM+ class: Presenting.CSharp.WhoisLookup"<br />

29:<br />

30: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}<br />

\InprocServer32]<br />

31: @="D:\WINNT\System32\MSCorEE.dll"<br />

32: "ThreadingModel"="Both"<br />

33: "Class"="Presenting.CSharp.WhoisLookup"<br />

34: "Assembly"="csharp, Ver=1.0.1.0"<br />

35:<br />

36: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}<br />

\ProgId]<br />

37: @="Presenting.CSharp.WhoisLookup"<br />

Wie ein Blick auf die Zeilen 30 bis 34 zeigt, wird zum Anlegen von Objektvariablen<br />

nicht die Bibliothek selbst, sondern die Execution Engine (MSCorEE.dll) aufgerufen.<br />

Sie ist hier in erster Linie für das Erzeugen einer COM-kompatiblen<br />

Hülle (CCW = COM Callable Wrapper) verantwortlich.<br />

Wenn Sie die Komponente ohne den Umweg über eine separate Registrierungsdatei<br />

direkt in die Registrierung eintragen wollen, reicht der Aufruf:<br />

regasm presenting.csharp.dll<br />

Nach dieser Registrierung lässt sich die Komponente in jeder Programmiersprache<br />

verwenden, die späte Bindung unterstützt. Wenn Sie mit später Bindung nicht<br />

zufrieden sind – was Sie übrigens auch nicht sein sollten –, verwenden Sie das<br />

Utility TlbExp zum Anlegen einer Typbibliothek für die NGWS-Komponente:<br />

tlbexp presenting.csharp.dll /out:presenting.csharp.tlb<br />

Diese Typbibliothek lässt sich mit Programmiersprachen verwenden, die frühe<br />

Bindung unterstützen, und macht die NGWS-Komponente sozusagen zu einem<br />

gesetzestreuen neuen Bürger im Staate COM.<br />

Und nachdem Sie damit endgültig in der Welt von COM angekommen sind, sollten<br />

Sie einen kurzen Ausflug mitten in die Typbibliothek unternehmen, weil sich


Kapitel 10 • Integration von nicht verwaltetem Code 165<br />

dabei einige wichtige Dinge zeigen lassen. Die in Listing 10.2 gezeigte IDL-Datei<br />

(Interface Description Language) lässt sich beispielsweise über das zu Visual Studio<br />

gehörige Utility OLE View gewinnen, indem Sie damit auf die (mit TlbExp<br />

erzeugte) Typbibliothek der NGWS-Komponente losgehen.<br />

Listing 10.2: Die IDL-Datei für die Klassen WhoisLookup und RequestWebPage<br />

1: // Generated .IDL file (by the OLE/COM Object Viewer)<br />

2: //<br />

3: // typelib filename: <br />

4:<br />

5: [<br />

6: uuid(A4466FD5-EB56-3C07-A0D8-43153AC4FD06),<br />

7: version(1.0)<br />

8: ]<br />

9: library csharp<br />

10: {<br />

11: // TLib : // TLib : : {BED7F4EA-1A96-11D2-8F08-<br />

00A0C9A6186D}<br />

12: importlib("mscorlib.tlb");<br />

13: // TLib : OLE Automation : {00020430-0000-0000-C000-<br />

000000000046}<br />

14: importlib("stdole2.tlb");<br />

15:<br />

16: // Forward declare all types defined in this typelib<br />

17: interface _RequestWebPage;<br />

18: interface _WhoisLookup;<br />

19:<br />

20: [<br />

21: uuid(6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3),<br />

22: custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},<br />

Presenting.CSharp.RequestWebPage")<br />

23: ]<br />

24: coclass RequestWebPage {<br />

25: [default] interface _RequestWebPage;<br />

26: interface _Object;<br />

27: };<br />

28:<br />

29: [<br />

30: odl,<br />

31: uuid(1E8F7AAB-FA6C-315B-9DFE-59C80C6483A9),<br />

32: hidden,<br />

33: dual,<br />

34: nonextensible,<br />

35: oleautomation,<br />

36: custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},<br />

"Presenting.CSharp.RequestWebPage")<br />

37:


166<br />

Zusammenarbeit mit COM<br />

38: ]<br />

39: interface _RequestWebPage : IDispatch {<br />

40: [id(00000000), propget]<br />

41: HRESULT ToString([out, retval] BSTR* pRetVal);<br />

42: [id(0x60020001)]<br />

43: HRESULT Equals(<br />

44: [in] VARIANT obj,<br />

45: [out, retval] VARIANT_BOOL* pRetVal);<br />

46: [id(0x60020002)]<br />

47: HRESULT GetHashCode([out, retval] long* pRetVal);<br />

48: [id(0x60020003)]<br />

49: HRESULT GetType([out, retval] _Type** pRetVal);<br />

50: [id(0x60020004), propget]<br />

51: HRESULT URL([out, retval] BSTR* pRetVal);<br />

52: [id(0x60020004), propput]<br />

53: HRESULT URL([in] BSTR pRetVal);<br />

54: [id(0x60020006)]<br />

55: HRESULT GetContent([out] BSTR* strContent);<br />

56: };<br />

57:<br />

58: [<br />

59: uuid(8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34),<br />

60: custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},<br />

"Presenting.CSharp.WhoisLookup")<br />

61: ]<br />

62: coclass WhoisLookup {<br />

63: [default] interface _WhoisLookup;<br />

64: interface _Object;<br />

65: };<br />

66:<br />

67: [<br />

68: odl,<br />

69: uuid(07255177-A6E5-3E9F-BAB3-1B3E9833A39E),<br />

70: hidden,<br />

71: dual,<br />

72: nonextensible,<br />

73: oleautomation,<br />

74: custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},<br />

"Presenting.CSharp.WhoisLookup")<br />

75:<br />

76: ]<br />

77: interface _WhoisLookup : IDispatch {<br />

78: [id(00000000), propget]<br />

79: HRESULT ToString([out, retval] BSTR* pRetVal);<br />

80: [id(0x60020001)]<br />

81: HRESULT Equals(<br />

82: [in] VARIANT obj,


Kapitel 10 • Integration von nicht verwaltetem Code 167<br />

83: [out, retval] VARIANT_BOOL* pRetVal);<br />

84: [id(0x60020002)]<br />

85: HRESULT GetHashCode([out, retval] long* pRetVal);<br />

86: [id(0x60020003)]<br />

87: HRESULT GetType([out, retval] _Type** pRetVal);<br />

88: };<br />

89: };<br />

Wenn Sie bereits mit C++ Erfahrungen gesammelt haben, sind Sie derartige<br />

Monstren wohl gewohnt. Für VB-ProgrammiererInnen stellt dieser Blick in die<br />

Innereien von IDL-Dateien dagegen vermutlich eine Erstbegegnung dar.<br />

Wie in den Zeilen 24 und 62 von Listing 10.2 zu sehen, verfügen beide Co-Klassen<br />

über eine von IDispatch abgeleitete Schnittstelle sowie eine Schnittstelle<br />

namens Object. Die Standardschnittstelle ist ebenfalls von IDispatch abgeleitet<br />

und enthält alle (öffentlichen) Methoden der jeweiligen NGWS-Komponente<br />

sowie die Methoden der Object-Schnittstelle. Außerdem dürfte auffallen, dass die<br />

Methoden nun sämtlich mit den allseits bekannten und beliebten Typen BSTR und<br />

VARIANT arbeiten.<br />

Abbildung 10.1 zeigt, wie OLE View die Schnittstelle von RequestWebPage darstellt,<br />

und bietet wenig Überraschendes: Die Eigenschaft URL steht über die getund<br />

set-Methoden zur Verfügung, außerdem gibt es offensichtlich vier Methoden<br />

der Object-Schnittstelle. Tatsächlich sieht die gesamte Definition fast genauso<br />

wie in C# aus:<br />

Bei der Schnittstelle für die Klasse WhoisLookup (siehe Abbildung 10.2) liegen die<br />

Verhältnisse ein klein wenig anders. Auch hier sind die vier Methoden der<br />

Object-Schnittstelle mit von der Partie – aber wo ist Query?<br />

Der Grund dafür, dass Query hier nicht erscheint: Diese Methode ist statisch,<br />

gehört also zur Klasse als Typ, nicht zu ihren Instanzen – und kann deshalb in<br />

COM nicht eingesetzt werden. Tatsächlich können Sie die Klasse WhoisLookup<br />

erst nach Umgestaltung der Methode Query in eine nicht statische Methode als<br />

COM-Objekt einsetzen. Leider gilt diese Beschränkung universell: Wenn Sie also<br />

planen, NGWS-Klassen gelegentlich auch außerhalb der NGWS-Laufzeitumgebung<br />

zu verwenden, sollten Sie sorgfältig abwägen, inwieweit Sie die unbestreitbaren<br />

Vorteile statischer Methoden überhaupt nutzen wollen.<br />

Aufruf eines NGWS-Objekts durch einen COM-Client<br />

Sobald die NGWS-Komponente registriert ist und Sie für Entwicklungsumgebungen<br />

mit früher Bindung eine Typbibliothek erzeugt haben, sind Sie für alle Fälle<br />

gerüstet. Wie einfach NGWS-Komponenten einsetzbar sind, lässt sich recht gut<br />

mit Microsoft Excel demonstrieren.


168<br />

Zusammenarbeit mit COM<br />

Bild 10.1: Die Klasse RequestWebPage mit der Eigenschaft URL und der Methode<br />

GetContent<br />

Bild 10.2: Statische Methoden sind nichts für COM.


Kapitel 10 • Integration von nicht verwaltetem Code 169<br />

Die einzige zusätzliche Vorbedingung für das Scripting von Komponenten in<br />

Excel (mit früher Bindung, wohlgemerkt!) ist eine Referenz auf die Typbibliothek.<br />

Starten Sie also den VBA-Editor (über Extras/Makro/Visual Basic-Editor),<br />

wählen Sie dort im Menü Extras den Befehl Verweise, klicken Sie im daraufhin<br />

erscheinenden Dialogfeld Verweise – VBA-Projekt auf Durchsuchen und wählen<br />

Sie die Typbibliothek der NGWS-Komponente aus.<br />

Bild 10.3: Import der Typbibliothek für die Komponente<br />

Wie Listing 10.3 zeigt, gestaltet sich eine Abfrage über RequestWebPage nach diesen<br />

Vorbereitungen recht geradlinig; die Behandlung von COM-Ausnahmen<br />

übernimmt hier ein On Error GoTo.<br />

Listing 10.3: Einsatz der Klasse RequestWebPage in einem Excel-Modul<br />

1: Option Explicit<br />

2:<br />

3: Sub GetSomeInfo()<br />

4: On Error GoTo Err_GetSomeInfo<br />

5: Dim wrq As New csharp.RequestWebPage<br />

6: Dim strResult As String<br />

7:<br />

8: wrq.URL = "http://www.alphasierrapapa.com/iisdev/"<br />

9: wrq.GetContent strResult<br />

10: Debug.Print strResult<br />

11:<br />

12: Exit Sub<br />

13: Err_GetSomeInfo:<br />

14: MsgBox Err.Description<br />

15: Exit Sub<br />

16: End Sub


170<br />

Zusammenarbeit mit COM<br />

Vielleicht werden Sie sich fragen, wie Visual Basic an die Ausnahmen des<br />

NGWS-Laufzeitsystems herankommt? Solche Ausnahmen werden erst einmal in<br />

entsprechende HRESULT-Werte umgesetzt, auf die Excel dann mit einer Fehlermeldung<br />

reagiert. Für die Übermittlung zusätzlicher Fehlerinformationen (wie der<br />

textuellen Beschreibung) sind wiederum andere Schnittstellen zuständig.<br />

Die in Listing 10.3 gezeigte Routine zeigt die abgerufenen HTML-Daten im<br />

Direktfenster von Excel an. Wenn Sie sehen wollen, wie das NGWS-Laufzeitsystem<br />

eine Ausnahme an einen COM-Client weiterreicht, probieren Sie es einmal<br />

mit einem absichtlich ungültigen URL.<br />

10.1.2 Verfügbarkeit von COM-Objekten für NGWS-Anwendungen<br />

Wie zu Anfang dieses Kapitels erwähnt, ist die Interoperabilität auch in der umgekehrten<br />

Richtung gegeben: NGWS-Clients können sich also auch existierender<br />

COM-Komponenten bedienen. In der Praxis wird man diesen Weg zumindest in<br />

der Übergangszeit von COM nach NGWS wohl recht häufig gehen.<br />

Auch im Zusammenhang mit NGWS-Clients gilt die für COM-Objekte typische<br />

Unterscheidung nach der Art der Bindung:<br />

• Aufruf von COM-Objekten mit früher Bindung<br />

• Aufruf von COM-Objekten mit später Bindung<br />

Die in diesem Abschnitt zur Demonstration verwendete COM-Komponente Asp-<br />

Touch ändert das Erstellungsdatum einer Datei. Sie hat eine duale Schnittstelle<br />

sowie eine Typbibliothek, ist Freeware – und Sie sind herzlich eingeladen, diese<br />

Komponente von http://www.alphasierrapapa.com/iisdev/components/ herunterzuladen,<br />

um die folgenden Erläuterungen auch praktisch nachzuvollziehen.<br />

Aufruf von COM-Objekten mit früher Bindung<br />

Für die frühe Bindung einer COM-Komponente wird eine Typbibliothek benötigt.<br />

In der NGWS-Laufzeitumgebung finden sich analoge Informationen in den Metadaten,<br />

die zusammen mit dem jeweiligen Typ gespeichert sind. Womit sich natürlich<br />

die Frage stellt, woher das NGWS-Laufzeitsystem die Typinformationen für<br />

eine COM-Komponente bezieht – und wie es mit nicht verwaltetem Code überhaupt<br />

zurechtkommt.<br />

Beide Aufgaben übernimmt eine Hülle um die COM-Komponente, die auch als<br />

RCW (Runtime Callable Wrapper) bezeichnet und mit Hilfe der Informationen<br />

aus der Typbibliothek aufgebaut wird. Für das Anlegen solcher Hüllen hält das<br />

NGWS SDK ein Dienstprogramm namens tlbimp (type library import) bereit,<br />

dessen Aufruf sich recht unspektakulär gestaltet:<br />

tlbimp asptouch.dll /out:asptouchlib.dll


Kapitel 10 • Integration von nicht verwaltetem Code 171<br />

Dieser Befehl liest die COM-Typbibliothek von asptouch.dll (die in dieser DLL<br />

als Ressource gespeichert ist), generiert mit diesen Informationen einen RCW<br />

und speichert ihn in der Datei asptouchlib.dll. Mit einem weiteren Dienstprogramm<br />

– ildasm.exe – können Sie untersuchen, welche Metadaten die neu angelegte<br />

DLL enthält (siehe Abbildung 10.4). Mehr zu ILDasm und den Möglichkeiten<br />

dieses Werkzeugs finden Sie in Kapitel 11, »C#-Code debuggen«.<br />

Bild 10.4: Die mit ILDasm dargestellten Metadaten von asptouchlib.dll<br />

Offensichtlich ist ASPTOUCHlib der Namensraum (der sich aus dem Namen der<br />

Typbibliothek ergibt) und TouchIt der Name der Stellvertreterklasse, die für die<br />

COM-Komponente erzeugt wurde. Mit diesen Informationen bewaffnet, können<br />

Sie nun einen NGWS-Client schreiben, der die COM-Komponente verwendet.<br />

Das folgende Listing gibt ein Beispiel:<br />

Listing 10.4: Einsatz einer COM-Komponente über einen RCW in einem C#-<br />

Programm<br />

1: using System;<br />

2: using ASPTOUCHLib;<br />

3:<br />

4: class TouchFile<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: TouchIt ti = new TouchIt();<br />

9: bool bResult = false;<br />

10: try<br />

11: {<br />

12: bResult = ti.SetToCurrentTime("touch.cs");


172<br />

Zusammenarbeit mit COM<br />

13: }<br />

14: catch(Exception e)<br />

15: {<br />

16: Console.WriteLine(e);<br />

17: }<br />

18: finally<br />

19: {<br />

20: if (true == bResult)<br />

21: {<br />

22: Console.WriteLine("Datum und Uhrzeit von touch.cs neu<br />

gesetzt.");<br />

23: }<br />

24: }<br />

25: }<br />

26: }<br />

Unterschiede zu anderen C#-Programmen werden Sie hier keine finden: Der<br />

Code enthält eine using-Anweisung, ruft Methoden der Klasse TouchIt auf, und<br />

behandelt Ausnahmezustände (die hier gegebenenfalls aus HRESULT-Werten generiert<br />

werden). Der Aufruf des Compilers enthält ebenfalls keine Besonderheiten:<br />

csc /r:asptouchlib.dll /out:touch.exe touch.cs<br />

Kurz und gut: Von RCWs umhüllte COM-Komponenten gleichen in jeder Hinsicht<br />

NGWS-Komponenten. Mehr gibt es dazu erfreulicherweise nicht zu sagen.<br />

Aufruf von COM-Objekten mit später Bindung<br />

Wenn Ihnen für eine COM-Komponente keine Typbibliothek zur Verfügung steht<br />

oder Ihr Programm aus anderen Gründen ohne RCW auskommen muss, hilft ein<br />

nettes NGWS-Feature weiter, das als Reflexion bezeichnet wird und es Ihrer<br />

Anwendung erlaubt, die notwendigen Informationen zur Laufzeit zu beschaffen.<br />

Das in Listing 10.5 wiedergegebene Programm demonstriert diese Technik: Es<br />

führt dieselben Aktionen aus wie das vorangehende Beispiel, kommt aber ohne<br />

eine Hüllklasse aus.<br />

Listing 10.5: Einsatz eines COM-Objekts über Reflexion<br />

1: using System;<br />

2: using System.Reflection;<br />

3:<br />

4: class TestLateBound<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: Type tTouch;<br />

9: tTouch = Type.GetTypeFromProgID("AspTouch.TouchIt");<br />

10:


Kapitel 10 • Integration von nicht verwaltetem Code 173<br />

11: Object objTouch;<br />

12: objTouch = Activator.CreateInstance(tTouch);<br />

13:<br />

14: Object[] parameters = new Object[1];<br />

15: parameters[0] = "reflect.cs";<br />

16: bool bResult = false;<br />

17:<br />

18: try<br />

19: {<br />

20: bResult = (bool)tTouch.InvokeMember("SetToCurrentTime",<br />

21: BindingFlags.InvokeMethod,<br />

22: null, objTouch, parameters);<br />

23: }<br />

24: catch(Exception e)<br />

25: {<br />

26: Console.WriteLine(e);<br />

27: }<br />

28:<br />

29: if (bResult)<br />

30: Console.WriteLine("Datum und Uhrzeit von reflect.cs neu<br />

gesetzt.");<br />

31: }<br />

32: }<br />

Die für die Reflexion eingesetzte Klasse hat den Namen Type und ist im Namensraum<br />

System.Reflection definiert. In Zeile 9 wird die Methode GetTypeFromProgID<br />

dieser Klasse mit der ProgId der fraglichen COM-Komponente aufgerufen:<br />

Sie sollte den tatsächlichen Typ ermitteln. Das Beispiel enthält der Übersichtlichkeit<br />

halber keine Ausnahmebehandlung, die man in ernsthaften Anwendungen<br />

aber auf jeden Fall einbauen sollte, weil GetTypeFromProgID eine Ausnahme signalisiert,<br />

falls sich der Typ nicht bestimmen und laden lässt.<br />

Nachdem der Typ geladen ist, lässt sich über die statische Methode CreateInstance<br />

der Klasse Activator ein Objekt dieses Typs erzeugen. Danach wird es leider<br />

etwas unübersichtlicher: Der Aufruf von Methoden über späte Bindung hat es<br />

nämlich in sich.<br />

Wer dieses Verfahren von C++ und COM her kennen und lieben gelernt hat, wird<br />

sich hier sofort zu Hause fühlen: Sämtliche Parameter – im gegebenen Beispiel<br />

der Dateiname – müssen in ein Array gepackt werden (Zeilen 14 und 15), der<br />

Aufruf der Methode geschieht indirekt über InvokeMember des Type-Objekts (Zeilen<br />

20 bis 22). Zu übergeben sind hier der Name der Methode, die Bindungsflags,<br />

ein Bindungsobjekt (oder eben NULL), das anzusprechende Objekt und das Parameter-Array.<br />

Das Funktionsergebnis dieses Aufrufs muss schließlich in einen entsprechenden<br />

Typ von C# bzw. der NGWS-Laufzeitumgebung umgewandelt werden.


174<br />

Aufruf plattformspezifischer Dienste<br />

Das sieht nicht nur übel aus, sondern ist es auch – obwohl das Beispiel eine ausgesprochen<br />

harmlose Variante darstellt. Wenn Sie Lust auf einen wirklich erbaulichen<br />

Nachmittag verspüren, dann probieren Sie es doch einmal mit der Übergabe<br />

von Referenzen.<br />

Kurz und schlecht: Man kann mit später Bindung arbeiten, wenn es denn gar nicht<br />

anders geht. Der Hauptgrund, wieso Sie wo immer möglich mit RCWs arbeiten<br />

sollten, ist allerdings nicht die einfachere Handhabung, sondern die Geschwindigkeit:<br />

Methodenaufrufe über späte Bindung sind um Größenordnungen zeitaufwändiger<br />

als bei früher Bindung.<br />

10.2 Aufruf plattformspezifischer Dienste<br />

Obwohl die NGWS-Klassen eine Menge zu bieten haben und es beim Einsatz von<br />

COM-Komponenten praktisch keine Grenzen gibt, werden Sie ab und an einmal<br />

auf Funktionen der Win32-API oder einer anderen nicht verwalteten DLL zurückgreifen<br />

wollen (oder müssen). Speziell für diesen Zweck wurden die Platform<br />

Invocation Services geschaffen, die in der SDK-Dokumentation unter dem Stichwort<br />

PInvoke zusammengefasst sind. Im Wesentlichen geht es dabei um das Heraussuchen<br />

und Aufrufen der gewünschten Funktion sowie die Übergabe von Parametern<br />

und Ergebnissen zwischen verwaltetem und nicht verwaltetem Code<br />

(Stichwort: Marshaling).<br />

Bei der Definition von extern-Methoden in C# geben Sie zusätzlich das Attribut<br />

sysimport an, dessen Syntax so aussieht:<br />

[sysimport(<br />

dll=dllname,<br />

name=Einsprungpunkt,<br />

charset=Zeichensatz<br />

)]<br />

Der Parameter dll ist obligatorisch, die beiden anderen sind optional. Wenn Sie<br />

das Attribut name nicht angeben, wird der Name eingesetzt, den Sie der Funktion<br />

im C#-Quelltext gegeben haben (und der dann seinerseits mit dem Namen der<br />

gewünschten Funktion aus der DLL übereinstimmen muss).<br />

Listing 10.6 zeigt am Beispiel der Win32-Funktion MessageBox, wie einfach sich<br />

die Sache gestalten kann.<br />

Listing 10.6: Aufruf einer Win32-Funktion über PInvoke<br />

1: using System;<br />

2:<br />

3: class TestPInvoke<br />

4: {<br />

5: [sysimport(dll="user32.dll")]


Kapitel 10 • Integration von nicht verwaltetem Code 175<br />

6: public static extern int MessageBoxA(int hWnd, string strMsg,<br />

7: string strCaption, int nType);<br />

8:<br />

9: public static void Main()<br />

10: {<br />

11: int nMsgBoxResult;<br />

12: nMsgBoxResult = MessageBoxA(0, "Hello C#", "PInvoke", 0);<br />

13: }<br />

14: }<br />

Zeile 5 gibt über das Attribut sysimport an, dass sich die gesuchte Funktion in<br />

user32.dll befindet. Da hier ein name= fehlt, verwendet der C#-Compiler den in<br />

der folgenden extern-Definition angegebenen Funktionsnamen (MessageBoxA,<br />

wobei das A für die ANSI-Version der Funktion steht). Das Ergebnis des Progrämmchens<br />

ist ein einfaches Meldungsfenster mit der Überschrift »PInvoke«<br />

und dem Text »Hello C#«.<br />

Wenn Sie den Namen der Funktion als Parameter von sysimport angeben, ist der<br />

interne Name der Funktion beliebig:<br />

Listing 10.7: Der name-Parameter von sysimport<br />

1: using System;<br />

2:<br />

3: class TestPInvoke<br />

4: {<br />

5: [sysimport(dll="user32.dll", name="MessageBoxA")]<br />

6: public static extern int PopupBox(int h, string m, string c, int<br />

type);<br />

7:<br />

8: public static void Main()<br />

9: {<br />

10: int nMsgBoxResult;<br />

11: nMsgBoxResult = PopupBox(0, "Hello C#", "PInvoke", 0);<br />

12: }<br />

13: }<br />

Auch wenn sich diese beiden Beispiele auf eine ausgesprochen einfache Win32-<br />

Funktion beschränken: Grenzen gibt es hier definitiv keine. Niemand hält Sie<br />

davon ab, mit dieser Technik auf Win32-Ressourcen zuzugreifen oder gar ein<br />

eigenes Marshaling zu implementieren. (Bei Vorhaben dieses Kalibers empfiehlt<br />

sich allerdings ein ausgiebiger Blick in die Dokumentation des NGWS-SDKs.)


176<br />

Unsicherer Code<br />

10.3 Unsicherer Code<br />

Unsicheren Code zu schreiben, dürfte sicher nicht Ihr tägliches Brot werden,<br />

wenn Sie mit C# programmieren. Falls Sie aber eben doch einmal Zeiger brauchen<br />

sollten, dann steht dem vom Prinzip her nichts im Wege. C# definiert in diesem<br />

Zusammenhang zwei Schlüsselwörter:<br />

• unsafe – kennzeichnet einen unsicheren Kontext und muss als Modifizierer<br />

für Elementfunktionen verwendet werden, die unsicheren Code enthalten. Anwendbar<br />

ist dieser Modifizierer auf Konstruktoren, Methoden und Eigenschaften.<br />

• fixed – kennzeichnet Variablen, die der Garbage Collector nicht verschieben<br />

soll<br />

Solange Sie nicht wirklich mit nackten Speicherbereichen arbeiten – also Zeiger<br />

verwenden – müssen, sollten Sie allerdings in fast allen Fällen mit Aufrufen plattformspezifischer<br />

Dienste und der von NGWS gebotenen COM-Interoperabilität<br />

auskommen, wenn es um Win32-Funktionen und/oder COM-Objekte geht.<br />

Um Ihnen dennoch eine Vorstellung davon zu vermitteln, wie die Schlüsselwörter<br />

unsafe und fixed praktisch eingesetzt werden, zeigt Listing 10.8 eine Klasse, die<br />

Quadrate so berechnet, wie man es von C und C++ her gewohnt ist. Weitere<br />

Details zu unsicherem Code finden Sie in der C#-Referenz des NGWS SDK.<br />

Listing 10.8: Ein Beispiel für unsicheren Code<br />

1: using System;<br />

2:<br />

3: public class SquareSampleUnsafe<br />

4: {<br />

5: unsafe public void CalcSquare(int nSideLength, int *pResult)<br />

6: {<br />

7: *pResult = nSideLength * nSideLength;<br />

8: }<br />

9: }<br />

10:<br />

11: class TestUnsafe<br />

12: {<br />

13: public static void Main()<br />

14: {<br />

15: int nResult = 0;<br />

16:<br />

17: unsafe<br />

18: {<br />

19: fixed(int* pResult = &nResult)<br />

20: {<br />

21: SquareSampleUnsafe sqsu = new SquareSampleUnsafe();


Kapitel 10 • Integration von nicht verwaltetem Code 177<br />

22: sqsu.CalcSquare(15, pResult);<br />

23: Console.WriteLine(nResult);<br />

24: }<br />

25: }<br />

26: }<br />

27: }<br />

10.4 Zusammenfassung<br />

In diesem Kapitel ging es ausschließlich um die verschiedenen Aspekte der<br />

Zusammenarbeit zwischen verwaltetem und nicht verwaltetem Code. NGWS-<br />

Komponenten lassen sich auch von COM-Clients aus benutzen, und COM-Komponenten<br />

stehen NGWS-Anwendungen zur Verfügung. Bei der – in allen Fällen<br />

empfehlenswerten – frühen Bindung von COM-Komponenten kommen Hüllklassen<br />

zum Einsatz, die über COM-Typbibliotheken erzeugt werden und die entsprechenden<br />

Informationen als Metadaten mitführen.<br />

Für den Aufruf plattformspezifischer Funktionen übernimmt das NGWS-Laufzeitsystem<br />

mit Pinvoke den Transfer von Parametern und Ergebnissen. Bei Bedarf<br />

steht C#-Anwendungen auf diese Weise die gesamte Win32-API (und jede andere<br />

exportierte DLL-Funktion) zur Verfügung.<br />

Im letzten Abschnitt des Kapitels ging es schließlich um unsicheren Code. Auch<br />

wenn verwalteter Code natürlich die bessere Wahl darstellt, können Sie bei<br />

Bedarf mit Zeigern arbeiten und Speicherbereiche fixieren – also all die Dinge<br />

tun, die verwaltetes C# sinnvollerweise nicht erlaubt.


Kapitel 11<br />

C#-Code debuggen<br />

11.1 Aufgabenfeld des Debuggers 180<br />

11.2 Der Intermediate Language Disassembler 193<br />

11.3 Zusammenfassung 195


180<br />

Aufgabenfeld des Debuggers<br />

Kommt es bei Ihnen oft vor, dass Sie Code schreiben, ihn ein- oder zweimal ausführen<br />

und, wenn er die gewünschte Ausgabe liefert, als »getestet« klassifizieren?<br />

Hoffentlich nicht. Ein anständiger Codetest sollte mindestens ein zeilenweises<br />

Durchlaufen des Quellcodes mit dem Debugger beinhalten und wird auch dann<br />

nur die Existenz von Fehlern nachweisen können, nicht jedoch Fehlerfreiheit.<br />

Das Debuggen von Code ist nicht nur die wichtigste sondern leider auch zeitaufwändigste<br />

Aufgabe bei der Entwicklung von Software. Wie nicht anders zu<br />

erwarten, stellt das NGWS-SDK verschiedene Werkzeuge bereit, mit denen Sie<br />

Ihren Code nach allen Regeln der Kunst analysieren und »entwanzen« können.<br />

Machen Sie Gebrauch davon! Dieses Kapitel demonstriert Ihnen den Einsatz der<br />

folgenden Werkzeuge:<br />

• SDK-Debugger<br />

• IL-Disassembler<br />

11.1 Aufgabenfeld des Debuggers<br />

Das zum NGWS-Subsystem gehörige SDK umfasst zwei Werkzeuge zur Fehlersuche:<br />

einen Kommandozeilen-Debugger namens CORDBG und den Benutzerschnittstellen-Debugger<br />

Microsoft URT-Debugger, der kurz SDK-Debugger genannt<br />

wird. Letzterer, eine abgespeckte Version des zu Visual Studio 7 gehörigen<br />

Debuggers, wird in diesem Abschnitt eingehender vorgestellt. Im Vergleich mit<br />

dem Debugger von Visual Studio weist der SDK-Debugger folgende Beschränkungen<br />

auf:<br />

• Der SDK-Debugger unterstützt keine Fehlersuche auf der Ebene von Maschinencode.<br />

Er ist auf verwalteten Code beschränkt.<br />

• Der SDK-Debugger unterstützt kein Remote Debugging von einer anderen Maschine<br />

aus. Für diesen Zweck müssen Sie auf den Debugger von Visual Studio<br />

zurückgreifen.<br />

• Das Register-Fenster ist zwar vorhanden, jedoch ohne Funktion.<br />

• Das Disassembler-Fenster ist zwar vorhanden, jedoch ohne Funktion.<br />

Diese Beschränkungen wirken sich nur dann als solche aus, wenn die Fehlersuche<br />

in gemischtsprachliche Umgebungen führt oder eben Remote Debugging erforderlich<br />

ist. Ansonsten bietet der SDK-Debugger die gleichen Möglichkeiten wie<br />

andere Debugger:<br />

• Ausführen einer Debug-Version der Anwendung<br />

• Auswählen der ausführbaren Datei<br />

• Setzen von Haltepunkten


Kapitel 11 • C#-Code debuggen 181<br />

• schrittweise Programmausführung<br />

• Einklinken in eine laufende Anwendung<br />

• Variablen inspizieren und modifizieren<br />

• Verwaltung von Ausnahmen<br />

• Just-in-time-Debugging<br />

• Fehlersuche in Komponenten<br />

11.1.1 Eine Debug-Version erzeugen<br />

Um den vollen Funktionsumfang des Debuggers zu nutzen, müssen Sie eine<br />

Debug-Version Ihrer Anwendung erstellen. Die Debug-Version enthält alle notwendigen<br />

Debug-Informationen, ist nicht optimiert und wird durch eine zusätzliche<br />

PDB-Datei (Programmdatenbank) ergänzt, in der alle Quelltextverweise und<br />

aktuellen Projektparameter aufgeführt sind.<br />

Um den Compiler zur Erstellung einer Debug-Version zu veranlassen, geben Sie<br />

zwei zusätzliche Schalter bei seinem Aufruf an:<br />

csc /optimize- /debug+ whilesample.cs<br />

Dieses Kommando erzeugt die beiden für den Aufruf des Debuggers geforderten<br />

Dateien whilesample.exe und whilesample.pdb. Das folgende Listing gibt den<br />

Quellcode von whilesample.cs wieder, der auch in den folgenden Abschnitten<br />

verschiedentlich erscheinen wird:<br />

Listing 11.1: Quelltext des in der Debugger-Sitzung analysierten Programms<br />

1: using System;<br />

2: using System.IO;<br />

3:<br />

4: class WhileDemoApp<br />

5: {<br />

6: public static void Main()<br />

7: {<br />

8: StreamReader sr = File.OpenText ("whilesample.cs");<br />

9: String strLine = null;<br />

10:<br />

11: while (null != (strLine = sr.ReadLine()))<br />

12: {<br />

13: Console.WriteLine(strLine);<br />

14: }<br />

15:<br />

16: sr.Close();<br />

17: }<br />

18: }


182<br />

Aufgabenfeld des Debuggers<br />

11.1.2 Auswählen der ausführbaren Datei<br />

Der erste Schritt in einer Debugger-Sitzung ist die Auswahl der Anwendung, für<br />

die die Fehlersuche erfolgen soll. Obwohl sich der Debugger auch in eine laufende<br />

Anwendung einklinken kann (mehr dazu später) ist der üblichere Fall der,<br />

dass Sie die Anwendung mit der Absicht starten, eine Fehlersuche durchzuführen.<br />

Auswahl und Start der Anwendung geschehen somit vom Fenster des Debuggers<br />

aus.<br />

Die Debug-Version des zu testenden Programms whilesample.exe wurde ja<br />

bereits im vorigen Abschnitt erzeugt. Für den Start des SDK-Debuggers müssen<br />

Sie die Anwendung DbgUrt.exe ausführen, die im Verzeichnis Laufw:\Program<br />

Files\NGWSSDK\GuiDebug zu finden ist. Abbildung 11.1 zeigt das Fenster des<br />

Debuggers.<br />

Bild 11.1: Das Fenster des Debuggers<br />

Um die ausführbare Datei für die Debugger-Sitzung auszuwählen, öffnen Sie über<br />

den Befehl Debug/Program to Debug das Dialogfeld Program To Debug (Abbildung<br />

11.2) und geben darin den Ausführungspfad der Anwendung sowie gegebenenfalls<br />

ein vom Anwendungsverzeichnis abweichendes Arbeitsverzeichnis an.


Kapitel 11 • C#-Code debuggen 183<br />

Bild 11.2: Auswahl der ausführbaren Datei für die Debuggersitzung<br />

Beachten Sie, dass Sie in dem Dialogfeld auch Kommandozeilenargumente angeben<br />

können, die der Debugger dem Programm beim Aufruf mitgibt. Da die Beispielanwendung<br />

nicht auf eine Auswertung von Kommandozeilenargumenten<br />

ausgelegt ist, können Sie das Textfeld leer lassen.<br />

Vom Prinzip her können Sie die Anwendung nach diesem Arbeitsgang sofort starten.<br />

In der Praxis werden Sie zuvor noch einen oder mehrere Haltepunkte an Stellen<br />

setzen, die Sie im Verlauf der Programmausführung überwachen wollen.<br />

11.1.3 Setzen von Haltepunkten<br />

Der SDK-Debugger unterstützt vier Arten von Haltepunkten:<br />

• Codebasierter Haltepunkt – der Debugger unterbricht das Programm, wenn der<br />

Code einer bestimmten Quelltextzeile zur Ausführung kommen soll.<br />

• Datenbasierter Haltepunkt – der Debugger unterbricht das Programm, wenn<br />

eine Variable (beispielsweise eine Schleifenvariable) einen spezifischen Wert<br />

annimmt.<br />

• Funktionsbasierter Haltepunkt – der Debugger unterbricht die Ausführung an<br />

einer spezifischen Stelle innerhalb einer Funktion.<br />

• Adressbasierter Haltepunkt – der Debugger unterbricht die Ausführung, wenn<br />

der Code eine spezifische Adresse im Hauptspeicher anspricht.<br />

Mit Abstand am häufigsten werden codebasierte Haltepunkte bei der Fehlersuche<br />

verwendet. Um beispielsweise einen solchen Haltepunkt in die Zeile 11 (also an<br />

den Beginn der Schleife) des Beispielprogramms zu setzen, gehen Sie wie folgt<br />

vor:


184<br />

Aufgabenfeld des Debuggers<br />

1. Öffnen Sie die Quelldatei whilesample.cs über den Befehl File/Open.<br />

2. Klicken Sie mit der rechten Maustaste in die bewusste Zeile und wählen Sie<br />

aus dem erscheinenden Kontextmenü den Befehl Insert Breakpoint. Das Fenster<br />

Ihres SDK-Debuggers sollte wie in Abbildung 11.3 aussehen: Ein roter<br />

Punkt am linken Bereichsrand zeigt an, dass die Zeile einen Haltepunkt enthält<br />

(nur datenbasierte Haltepunkte sind nicht auf diese Weise gekennzeichnet).<br />

Bild 11.3: Einen Haltepunkt im SDK-Debugger einfügen<br />

Das war es bereits. Falls Sie die Eigenschaften des Haltepunkts bearbeiten wollen,<br />

wählen Sie den Befehl Breakpoint Properties im Kontextmenü der Zeile. Der<br />

daraufhin erscheinende Dialog gestattet es Ihnen, eine Bedingung für den Haltepunkt<br />

festzulegen sowie eine Zählung zu aktivieren – mit dem Effekt, dass der<br />

Debugger den Code erst dann unterbricht, wenn die Bedingung das n-te Mal<br />

erfüllt ist.<br />

Im Fenster Breakpoints, das über den Befehl Windows/Breakpoints im Menü<br />

Debug geöffnet wird, erhalten Sie einen Überblick über alle gesetzten Haltepunkte<br />

sowie deren Bedingungen (vgl. Abbildung 11.4).


Kapitel 11 • C#-Code debuggen 185<br />

Bild 11.4: Inspizieren eines Haltepunkts im Fenster Breakpoints<br />

Nachdem der Haltepunkt definiert ist, können Sie das Programm über den Debugger<br />

starten. Dazu geben Sie entweder den Befehl Start im Menü Debug oder klicken<br />

auf das gleichnamige Symbol (Rechtspfeil) in der Symbolleiste. Wie nicht<br />

anders zu erwarten, unterbricht der Debugger das Programm, sobald die Ausführung<br />

den Haltepunkt erreicht hat. Nun können Sie das Programm schrittweise<br />

ausführen.<br />

11.1.4 Schrittweise Programmausführung<br />

Wenn der Debugger Ihre Anwendung an einem Haltepunkt angehalten hat, können<br />

Sie die Ausführung der Anwendung auf verschiedene Arten wieder aufnehmen.<br />

Die entsprechenden Befehle finden Sie in der Symbolleiste sowie im Menü<br />

des Debuggers:<br />

• Step Over – der Debugger führt die nächste Anweisung en bloc aus und unterbricht<br />

danach die Ausführung wieder.<br />

• Step Into – der Debugger führt die nächste Einzelanweisung aus und unterbricht<br />

danach die Ausführung wieder. Im Gegensatz zu Step Over lässt sich die<br />

Ausführung mit Step Into auch in Funktionskörper hinein verfolgen.<br />

• Step Out – der Debugger setzt die Ausführung des Programms bis zum Rücksprung<br />

der aktuellen Funktion fort.<br />

• Run to Cursor – der Debugger setzt die Ausführung des Programms bis zu der<br />

Anweisung fort, an der die Eingabemarke im Quelltext steht, beachtet aber<br />

auch zuvor gelegene Haltepunkte.<br />

Probieren Sie die verschiedenen Befehle aus und beenden Sie dann den Debugger.


186<br />

Aufgabenfeld des Debuggers<br />

11.1.5 Einklinken in eine laufende Anwendung<br />

Der SDK-Debugger kann auch die Kontrolle über Anwendungen übernehmen,<br />

die er nicht selbst gestartet hat. In diesem Fall wählen Sie die Anwendung aus der<br />

Liste der laufenden Prozesse aus. Das funktioniert beispielsweise für Anwendungen,<br />

die gerade auf eine Benutzereingabe warten oder als Dienst ausgeführt werden<br />

– wichtig ist, dass der Debugger noch die Zeit hat, sich in die Anwendung<br />

einzuklinken, bevor sie endet.<br />

Damit Sie sehen, wie das funktioniert, finden Sie im folgenden Listing noch einmal<br />

das do…while-Beispiel aus Kapitel 6 abgedruckt, das den Benutzer in einer<br />

Schleife auffordert, Zahlen zur Berechnung des arithmetischen Mittels einzugeben:<br />

Listing 11.2: Die Datei attachto.cs ist für den nachträglichen Aufruf des<br />

Debuggers geeignet<br />

1: using System;<br />

2:<br />

3: class ComputeAverageApp<br />

4: {<br />

5: public static void Main()<br />

6: {<br />

7: ComputeAverageApp theApp = new ComputeAverageApp();<br />

8: theApp.Run();<br />

9: }<br />

10:<br />

11: public void Run()<br />

12: {<br />

13: double dValue = 0;<br />

14: double dSum = 0;<br />

15: int nNoOfValues = 0;<br />

16: char chContinue = 'j';<br />

17: string strInput;<br />

18:<br />

19: do<br />

20: {<br />

21: Console.Write("Bitte eine Zahl eingeben: ");<br />

22: strInput = Console.ReadLine();<br />

23: dValue = Double.Parse(strInput);<br />

24: dSum += dValue;<br />

25: nNoOfValues++;<br />

26: Console.Write("Noch eine Zahl (j/n)? ");<br />

27:<br />

28: strInput = Console.ReadLine();<br />

29: chContinue = Char.FromString(strInput);<br />

30: }<br />

31: while ('j' == chContinue);


Kapitel 11 • C#-Code debuggen 187<br />

32:<br />

33: Console.WriteLine("Arithmetisches Mittel: {0}", dSum / nNoOf-<br />

Values);<br />

34: }<br />

35: }<br />

Nachdem Sie das Programm mit<br />

csc /optimize- /debug+ attachto.cs<br />

kompiliert haben, können Sie es ganz normal über die Kommandozeile starten.<br />

Das Programm gibt dann an der Konsole die Meldung Bitte eine Zahl eingeben<br />

aus und wartet auf die nächste Benutzereingabe – eine gute Gelegenheit, den<br />

Debugger zu starten und ihm die Kontrolle über das Programm zu übertragen.<br />

Dazu öffnen Sie das Dialogfeld Programs über den gleichnamigen Befehl im<br />

Debug-Menü und markieren darin die Anwendung, deren Kontrolle der Debugger<br />

übernehmen soll (vgl. Abbildung 11.5). Beachten Sie aber, dass sich der SDK-<br />

Debugger nur in solche Anwendungen einklinken kann, die dem Typ COM+<br />

angehören – also NGWS-Anwendungen sind.<br />

Bild 11.5: Einklinken des Debuggers in ein laufendes Programm<br />

Wenn Sie nach einem Klick auf die Schaltfläche Attach das danach erscheinende<br />

Dialogfeld Attach to Process mit OK bestätigen, ändert sich die Anzeige im Dialogfeld<br />

Programs dahingehend, dass die gewählte Anwendung nun im Listenfeld


188<br />

Aufgabenfeld des Debuggers<br />

Debugged Processes als Eintrag erscheint. Markiert man diesen Eintrag und<br />

klickt auf die Schaltfläche Detach, endet die Kontrolle des Debuggers über die<br />

Anwendung (natürlich endet sie in jedem Fall auch, wenn die Anwendung oder<br />

der Debugger beendet wird).<br />

Bild 11.6: Der Debugger kann jederzeit wieder ausgeklinkt werden<br />

Nach dem Attach-Vorgang klicken Sie auf die Schaltfläche Break, um die Anwendung<br />

zu unterbrechen, und schließlich auf Close. Sobald der Debugger die Programmausführung<br />

unterbrochen hat, öffnet er die Quellcodedatei und markiert<br />

darin die als Nächstes auszuführende Anweisung (Zeile 21). Schalten Sie zurück<br />

auf die Anwendung und geben Sie eine Zahl ein.<br />

Der nächste Abschnitt fährt mit diesem Beispiel fort und zeigt Ihnen, wie Sie in<br />

einer Debugger-Sitzung die Werte einzelner Variablen erkunden und manipulieren.<br />

11.1.6 Variablen inspizieren und modifizieren<br />

Wenn Sie nun auf das Fenster des SDK-Debuggers zurückschalten, werden Sie<br />

feststellen, dass dieser immer noch in Zeile 21 verharrt. Platzieren Sie die Eingabemarke<br />

in Zeile 26 und geben Sie den Befehl Run to Cursor. Der Debugger liest<br />

nun die Zahl, die Sie zuvor eingegeben haben, und aktualisiert die Variablen dSum<br />

und nNoOfValues.


Kapitel 11 • C#-Code debuggen 189<br />

Um die Werte der beiden Variablen zu inspizieren, öffnen Sie das Fenster Locals<br />

über den Befehl Windows/Locals im Menü Debug. Wie in Abbildung 11.7<br />

gezeigt, gibt das Fenster tabellarisch die Namen, Werte und Typen aller lokalen<br />

Variablen für die aktuelle Aufrufebene (Methode) wieder.<br />

Bild 11.7: Das Fenster Locals listet die lokalen Variablen der aktuellen Aufrufebene<br />

auf<br />

Wenn Sie den Wert einer Variable ändern wollen, doppelklicken Sie auf ihren<br />

Wert in der Spalte Value und geben dann den neuen Wert ein. Mehr ist nicht zu<br />

tun. Von nun an arbeitet das Programm mit dem neuen Wert.<br />

Ein weiteres Mittel für die Beobachtung von Variablen und Ausdrücken ist das<br />

Fenster Watch. Im Gegensatz zum Fenster Locals ist Watch zu Beginn noch leer,<br />

bietet dafür aber die Möglichkeit, Variablen und Ausdrücke eigener Wahl als Einträge<br />

einzufügen. Auch bleiben die Einträge dann unabhängig von der Aufrufebene<br />

die ganze Zeit über sichtbar, selbst wenn die darin aufgeführten Variablen<br />

und Ausdrücke nicht auswertbar sind.<br />

11.1.7 Verwaltung von Ausnahmen<br />

Ein wirklich nützliches Feature des SDK-Debuggers ist die Möglichkeit, Regeln<br />

für den Umgang mit Ausnahmen festlegen zu können. Die diesbezügliche Konfi-


190<br />

Aufgabenfeld des Debuggers<br />

guration des Debuggers geschieht (nach Auswahl der auszuführenden Anwendung)<br />

im Dialogfeld Exceptions, dessen Aufruf über den Menübefehl Debug/<br />

Exceptions erfolgt (vgl. Abbildung 11.8). Der Dialog gestattet es, für jede Ausnahme<br />

(im Rahmen der bestehenden Klassenhierarchie) einzeln festzulegen, wie<br />

der Debugger damit verfahren soll.<br />

Bild 11.8: Die Reaktion des SDK-Debuggers auf verschiedene Ausnahmen festlegen<br />

Voreinstellung für alle Ausnahmen ist die Option Continue im oberen Optionenblock<br />

und die Option Break into the debugger im unteren Optionenblock. Das<br />

heißt, dass der Debugger bei Auftreten einer Ausnahme zunächst einmal nichts<br />

unternimmt und erst in Aktion tritt, wenn die Anwendung keine Behandlung der<br />

Ausnahme vornimmt.<br />

Obwohl Sie mit dieser Voreinstellung alle Ausnahmen mitbekommen, die Ihr<br />

Code nicht behandelt, werden Sie für den einen oder anderen Fall vielleicht eine<br />

andere Einstellung vorziehen, etwa um die Ausführung fortzusetzen, wenn eine<br />

Wertebereichsverletzung für ein Argument auftritt oder wenn Sie eine FileIO-<br />

Exception-Ausnahme auf jeden Fall mitbekommen möchten, selbst wenn Ihr<br />

Code eine Behandlungsroutine dafür bereitstellt.


Kapitel 11 • C#-Code debuggen 191<br />

11.1.8 Just-in-Time-Debugging<br />

Eine unbehandelte Ausnahme ist natürlich ein hervorragender Ausgangspunkt für<br />

eine Debugger-Sitzung. Das NGWS-Laufzeitsystem konfrontiert Sie in diesem<br />

Fall mit einem Dialogfeld (vgl. Abbildung 11.9), das es Ihnen erlaubt, die Kontrolle<br />

einem Debugger Ihrer Wahl zu übergeben. Man nennt dieses Vorgehen Justin-time-Debugging<br />

(JIT), da der Debugger »gerade zum richtigen Zeitpunkt« zum<br />

Einsatz kommt.<br />

Bild 11.9: Das Dialogfeld Just-in-Time Debugging erscheint bei Auftreten einer<br />

unbehandelten Ausnahme<br />

Wenn Sie das Dialogfeld über die Schaltfläche Yes beenden, klinkt sich der SDK-<br />

Debugger in das Programm ein, öffnet den Quellcode und markiert darin die<br />

Zeile, die für die Ausnahme verantwortlich ist. Darüber hinaus informiert Sie ein<br />

weiteres Dialogfeld (das diesmal der Debugger anzeigt) noch einmal über die Art<br />

der aufgetretenen Ausnahme (Abbildung 11.10).<br />

Nachdem Sie das Dialogfeld weggeklickt haben, stehen Ihnen alle in diesem<br />

Kapitel vorgestellten Techniken zur Verfügung, um dem Fehler auf den Grund zu<br />

gehen.


192<br />

Aufgabenfeld des Debuggers<br />

Bild 11.10: Der Debugger meldet, welche Ausnahme den Just-in-time-Aufruf<br />

bewirkt hat<br />

11.1.9 Fehlersuche in Komponenten<br />

Die Fehlersuche in C#-Komponenten folgt dem gleichen Prinzip wie die Fehlersuche<br />

in Komponenten, die in C++ geschrieben sind: Der Debugger wird für eine<br />

Client-Anwendung aufgerufen, die von der Komponente Gebrauch macht. Der<br />

Rest ist nicht anders als bei einer Anwendung: Sie setzen Haltepunkte in den<br />

Quellcode der Anwendung oder warten auf eine Ausnahme. Um eine Komponente<br />

zu debuggen, muss die Client-Anwendung nicht unbedingt als Debug-Version<br />

vorliegen (auch wenn es zu empfehlen ist), es reicht bereits, wenn eine<br />

Debug-Version der Komponente verfügbar ist.<br />

Um die Debug-Version einer Bibliothek zu erstellen, geben Sie wie gehabt die<br />

Schalter /debug+ und /optimize- an. Für die Beispielkomponente aus Kapitel 8,<br />

»Komponenten mit C# schreiben« würde der Compileraufruf also wie folgt lauten:<br />

csc /r:System.Net.dll /t:library /out:csharp.dll /a.version:1.0.1.0<br />

/debug+ /optimize- whoislookup.cs requestwebpage.cs


Kapitel 11 • C#-Code debuggen 193<br />

Die (wie gesagt nicht unbedingt erforderliche) Debug-Version der Client-Anwendung<br />

übersetzen Sie wie gehabt:<br />

csc /r:csharp.dll /out:wrq.exe /debug+ /optimize- whoisclient.cs<br />

Es ist Ihnen nun freigestellt, ob Sie den Debugger sofort starten und die Client-<br />

Anwendung aus seinem Fenster heraus ausrufen, oder ob Sie ihn erst später<br />

(gegebenenfalls just-in-time) in die Anwendung einklinken. Wenn sowohl die Client-Anwendung<br />

also auch die Komponente als verwalteter Code und als Debug-<br />

Version vorliegen, können Sie der Ausführung mit dem Debugger schrittweise<br />

von der Client-Anwendung in die Komponente und wieder zurück folgen.<br />

11.2 Der Intermediate Language Disassembler<br />

Ein hübsches kleines Werkzeug aus der Werkzeugkiste des NGWS SDK ist der<br />

Intermediate Language Disassembler, kurz IL-Disassembler oder ILDasm. Trotz<br />

seiner Bestimmung, die dieser Name ohne Zweifel suggeriert, kann der IL-Disassembler<br />

auch dafür verwendet werden, wichtige Informationen über die Metadaten<br />

und Ausprägung von ausführbarem NGWS-Code darzustellen. So lohnt sich<br />

der Einsatz des Werkzeugs beispielsweise, wenn Sie einen RCW (Runtime Callable<br />

Wrapper) für eine COM-Komponente implementiert haben und ein wenig<br />

mehr über die Hüllklasse in Erfahrung bringen wollen.<br />

Gestartet wird der IL-Disassembler über das Untermenü Tools im Startmenü<br />

des Microsoft NGWS SDK. Nach Auswahl einer NGWS-Komponente über<br />

den Befehl File/Open präsentiert sein Fenster die Typhierarchie dieser Komponente<br />

und ermöglicht das interaktive Durchforsten ihres Namensraums (vgl.<br />

Abbildung 11.11).<br />

Ein Doppelklick auf den Eintrag MANIFEST enthüllt, welche Bibliotheken die<br />

Komponente importiert sowie verschiedene Informationen über die vorliegende<br />

Implementation (Versionsnummer etc.) der Komponente.<br />

Was fortgeschrittene ProgrammierInnen jedoch wirklich interessieren wird:<br />

ILDasm macht wirklich den IL-Code sichtbar, den der Compiler erzeugt hat (vgl.<br />

Abbildung 11.12). Und nachdem das Fenster zu jeder IL-Code-Sequenz auch den<br />

ursprünglichen C#-Code anzeigt (jedoch nur, wenn eine Debug-Version vorliegt),<br />

lässt sich die Arbeitsweise des IL recht einfach erlernen. Eine Dokumentation der<br />

einzelnen IL-Instruktionen finden Sie im NGWS SDK.


194<br />

Der Intermediate Language Disassembler<br />

Bild 11.11: Durchforsten des Namensraums einer NGWS-Komponente mit ILDasm<br />

Bild 11.12: IL-Ansicht der Methode GetContent


Kapitel 11 • C#-Code debuggen 195<br />

11.3 Zusammenfassung<br />

Dieses Kapitel hat Ihnen den zum NGWS SDK gehörigen SDK-Debugger vorgestellt.<br />

Nachdem es sich bei diesem Debugger um eine leicht abgespeckte Version<br />

des zu Visual Studio gehörigen Debuggers handelt, ist der Funktionsumfang ausgesprochen<br />

reichhaltig – was die Fehlersuche natürlich sehr effizient macht.<br />

Sie eröffnen eine Debugger-Sitzung für eine Anwendung entweder, indem Sie die<br />

Anwendung vom Fenster des Debuggers aus starten, oder, indem Sie den Debugger<br />

sich später in die bereits in Ausführung befindliche Anwendung einklinken<br />

lassen (»Attach«). Wenn der Debugger einen Haltepunkt erreicht, unterbricht er<br />

das Programm und gibt Ihnen die Möglichkeit, die Ausführung schrittweise, bis<br />

zur Position der Einfügemarke im Quelltext oder bis zum nächsten Haltepunkt<br />

fortzusetzen. An jedem Haltepunkt besteht die Möglichkeit, die Werte von Variablen<br />

nicht nur zu inspizieren, sondern gegebenenfalls auch zu manipulieren.<br />

Besondere Flexibilität bietet der Debugger auch im Umgang mit Ausnahmen: Sie<br />

können für jede Ausnahme getrennt festlegen, ob und wann der Debugger darauf<br />

reagieren soll.<br />

Wer ein Freund von Assembler ist, dürfte im IL-Disassembler ein wertvolles<br />

Werkzeug finden, das ihm nicht nur die IL und die Umsetzung von C#-Quellcode<br />

in IL-Instruktionen nahebringt, sondern auch eine Fülle von Informationen über<br />

die Komponente selbst und ihre Metadaten zugänglich macht.


Kapitel 12<br />

Sicherheit<br />

12.1 Codezugriffsrechte 198<br />

12.2 Rollenbasierte Sicherheit 202<br />

12.3 Zusammenfassung 203


198<br />

Codezugriffsrechte<br />

Sicherheit ist ein wichtiges Thema – vor allem in einer Welt, in der Code aus<br />

einer Vielzahl von Quellen inklusive des Internet kommt. Kein Wunder also, dass<br />

diesem Punkt beim Design von NGWS erhebliche Aufmerksamkeit gewidmet<br />

wurde: Sicherheitsanforderungen werden hier sowohl auf der Codeebene als auch<br />

auf der Ebene der Benutzerrechte erfüllt.<br />

Da sich über die vom NGWS-Subsystem gebotene Sicherheit leicht ein eigenes<br />

Buch schreiben ließe, beschränkt sich dieses Kapitel darauf, Ihnen die Konzepte<br />

und Möglichkeiten im Überblick vorzustellen. Codebeispiele fehlen auf den folgenden<br />

Seiten komplett: Da sie notwendigerweise kurz ausfallen müssten, würden<br />

sie zwangsläufig den Eindruck erwecken, Sicherheit sei bei den NGWS eine<br />

Art Nachgedanke. Und das ist sie nun wirklich nicht.<br />

Die folgenden Seiten behandeln daher zwei Themenbereiche ausschließlich aus<br />

konzeptueller Sicht:<br />

• Codezugriffsrechte<br />

• Rollenbasierte Sicherheit<br />

12.1 Codezugriffsrechte<br />

Code gelangt heutzutage nicht mehr ausschließlich über ein vom firmeneigenen<br />

Server gestartetes Installationsprogramm auf die eigene Festplatte, sondern auch<br />

über Webseiten aus dem Internet und über E-Mails. Die Erfahrungen der jüngsten<br />

Zeit haben gezeigt, dass Code aus diesen Quellen recht gefährlich sein kann. Wie<br />

begegnet das NGWS-Subsystem dieser potenziellen Bedrohung?<br />

Eine der von NGWS in dieser Hinsicht angebotenen Lösungen für die Sicherheit<br />

sind die Codezugriffsrechte: Code genießt je nach Art und Herkunft ein unterschiedlich<br />

hohes Vertrauen, und das Prädikat »uneingeschränkt vertrauenswürdig«<br />

wird nur einem ausgesprochen kleinen Teil des Codes erteilt.<br />

Die wichtigsten Elemente dieses Sicherheitskonzepts sind:<br />

• Administratoren können Sicherheitsrichtlinien festlegen, die Code und Codegruppen<br />

bestimmte Rechte erteilen.<br />

• Code kann seinerseits fordern, dass ein Aufrufer bestimmte Rechte haben<br />

muss.<br />

• Die Ausführung von Code wird durch das Laufzeitsystem eingeschränkt: Es<br />

prüft, ob die einem Aufrufer zugestandenen Rechte für die Ausführung bestimmter<br />

Operationen ausreichen.<br />

• Code kann Rechte anfordern, die zu seiner Ausführung unbedingt notwendig<br />

sind, sowie weitere Rechte, die sinnvoll wären; darüber hinaus kann er auch explizit<br />

bekannt geben, welche Rechte er in keinem Fall benötigt.


Kapitel 12 • Sicherheit 199<br />

• Für Zugriffe auf verschiedene Systemressourcen sind individuelle Rechte definiert,<br />

die sich wahlweise vergeben und entziehen lassen.<br />

• Rechte werden beim Laden von Komponenten vergeben bzw. geprüft. Die<br />

Vergabe dieser Rechte basiert auf den Anforderungen der jeweiligen Komponente<br />

sowie den Sicherheitsrichtlinien.<br />

Kurz und gut: Code mit geringerer Sicherheitseinstufung kann effektiv daran<br />

gehindert werden, Code mit höherer Sicherheitseinstufung aufzurufen, weil für<br />

ihn eben nur eine Teilmenge der Rechte gilt. Eine solche Einigung auf den kleinsten<br />

gemeinsamen Nenner ist vor allem bei Internetzugriffen wichtig.<br />

Technisch gesehen, bauen Codezugriffsrechte auf zwei Schlüsselkonzepte auf:<br />

der Prüfung der Typensicherheit für verwalteten Code und der Möglichkeit, explizit<br />

bestimmte Rechte anzufordern. Woraus auch folgt: Wenn Sie die Vorteile dieses<br />

Sicherheitskonzepts genießen wollen, müssen Sie im Minimalfall typensicheren<br />

Code schreiben.<br />

12.1.1 Prüfung der Typensicherheit<br />

Der erste Schritt der Laufzeitumgebung bei der Durchsetzung von Zugriffsbeschränkungen<br />

besteht aus der Prüfung, ob der Code typensicher ist oder nicht. Die<br />

Typensicherheit stellt die Voraussetzung für eine zuverlässige Prüfung der Rechte<br />

eines Aufrufers dar.<br />

Technisch gesehen, steht hinter der Überwachung von Zugriffsrechten bei Aufrufen<br />

ein schrittweises Durcharbeiten des Stacks – und das setzt wiederum voraus,<br />

dass alle dort verzeichneten Typen so gehalten sind, dass der Code auf sie nur in<br />

exakt festgelegter Weise zugreifen kann.<br />

Erfreulicherweise erfüllt C# die Forderung nach Typensicherheit von selbst –<br />

jedenfalls, solange Sie in eine Klasse nicht explizit unsicheren Code einbauen.<br />

Das Laufzeitsystem prüft übrigens nicht nur den Zwischencode, sondern auch die<br />

Metadaten, bevor es Code niedrigerer Sicherheitseinstufung den Aufruf von Code<br />

mit höherer Sicherheitseinstufung erlaubt.<br />

12.1.2 Rechte<br />

Der nächste Schritt besteht aus der expliziten Anforderung von Rechten. Welche<br />

Vorteile eine solche aktive Vorgehensweise bringt? Wenn Ihr Code Rechte für<br />

seine Aktionen zu einem von Ihnen festgelegten Zeitpunkt anfordert, dann wissen<br />

Sie als Verfasser, ab wann bestimmte Aktionen zulässig sind, beziehungsweise<br />

können gegebenenfalls alternative Ausführungspfade für den Fall vorsehen, dass<br />

das System dem Code nur einen Teil der gewünschten Rechte zugesteht. Außerdem<br />

lässt sich auf diese Weise klar festlegen, welche Rechte ein bestimmter Programmteil<br />

nicht braucht. Code, der lediglich minimale Rechte einfordert, wird


200<br />

Codezugriffsrechte<br />

auch auf Systemen mit hohen Sicherheitsanforderungen ausgeführt. Code, der<br />

aufs Geratewohl hin erst einmal alle Rechte haben will, dürfte auf solchen Systemen<br />

scheitern.<br />

Die Kategorien, in die Rechte in diesem Zusammenhang fallen, wurden bereits<br />

implizit erwähnt:<br />

• notwendig – Rechte, die Ihr Code zur Ausführung unbedingt benötigt<br />

• optional – Rechte, die nicht unbedingt notwendig sind, die zu bewältigende<br />

Aufgabe aber vereinfachen würden<br />

• zurückgewiesen – Rechte, die Ihr Code auch dann nicht bekommen soll, wenn<br />

die Sicherheitsrichtlinien das zulassen würden. Mit einer solchen expliziten<br />

Eingrenzung können Sie die potenziellen Sicherheitslöcher klein halten.<br />

Die interessante Frage ist natürlich, welche Rechte sich explizit anfordern lassen,<br />

welche implizit über die Einstufung des Codes und welche schließlich über die<br />

Identität des Benutzers vergeben werden. Die Antworten auf die ersten beiden<br />

Teilpunkte geben die folgenden Abschnitte:<br />

• Standardrechte<br />

• Code-Identität<br />

Mit dem dritten Punkt setzt sich der Abschnitt »Rollenbasierte Sicherheit« auseinander.<br />

Standardrechte<br />

Vom NGWS-Laufzeitsystem zur Verfügung gestellte Ressourcen werden über<br />

Codezugriffsrechte geschützt. Mit entsprechenden Rechten dieser Art ausgestattet,<br />

erhalten Programme Zugriff auf die jeweilige Ressource und/oder die Erlaubnis,<br />

bestimmte (geschützte) Operationen mit ihr auszuführen. Ihr Code kann jedes<br />

dieser Rechte zur Laufzeit explizit anfordern, und die Laufzeitumgebung entscheidet,<br />

ob das jeweilige Recht zugestanden wird oder nicht.<br />

Tabelle 12.1 listet die Standardrechte zusammen mit einer kurzen Beschreibung<br />

auf. Wie dort zu sehen, haben beispielsweise die Net-Klassen ein eigenes<br />

Zugriffsrecht für die Netzwerksicherheit.<br />

Recht<br />

EnvironmentPermission<br />

Tab. 12.1: Standardrechte<br />

Beschreibung<br />

Diese Klasse definiert Zugriffsrechte auf Umgebungsvariablen.<br />

Zwei Arten des Zugriffs sind hier<br />

definiert: Lesen und Schreiben, wobei Schreiben<br />

auch das Anlegen und Löschen von Umgebungsvariablen<br />

abdeckt.


Kapitel 12 • Sicherheit 201<br />

Recht<br />

FileDialogPermission<br />

FileIOPermission<br />

IsolatedStoragePermission<br />

ReflectionPermission<br />

RegistryPermission<br />

SecurityPermission<br />

UIPermission<br />

Tab. 12.1: Standardrechte (Forts.)<br />

Beschreibung<br />

kontrolliert den Zugriff auf Dateien über die Standarddialogfelder<br />

zum Öffnen und Speichern von<br />

Dateien. Der Benutzer muss die vom Code<br />

gewünschten Zugriffe über ein Dialogfeld explizit<br />

autorisieren.<br />

definiert für Dateioperationen drei verschiedene<br />

Rechte: Lesen, Schreiben und Anhängen. Leserechte<br />

beinhalten die Abfrage von Dateiinformationen,<br />

Schreibrechte das Löschen und Überschreiben von<br />

Dateien. Beim dritten Recht geht es um das Anhängen<br />

neuer Daten an eine existierende Datei – was das<br />

Lesen des bereits vorhandenen Dateiinhalts nicht<br />

einschließt.<br />

kontrolliert den Zugriff auf benutzerindividuelle<br />

Daten (die ihrerseits ein mit NGWS neu eingeführtes<br />

Konzept darstellen). Unter anderem lassen sich hier<br />

Quoten vergeben und die Dauer der Speicherung<br />

festlegen.<br />

erlaubt bzw. verwehrt die Möglichkeit zur Abfrage<br />

von Typinformationen für nicht öffentliche Elemente<br />

und ist außerdem für Reflection.Emit zuständig.<br />

kontrolliert das Lesen, Anlegen und Ändern von<br />

Schlüsseln und Werten in der Registrierung des Systems.<br />

Jedes der gewünschten Rechte muss hier separat<br />

und zusammen mit einer Liste der Schlüssel bzw.<br />

Werte angegeben werden.<br />

stellt eine Sammlung einfacher Flags für das Sicherheitssystem<br />

dar. Unter anderem können Sie hier die<br />

Ausführung von Code kontrollieren, Sicherheitsprüfungen<br />

außer Kraft setzen, Aufrufe nicht verwalteten<br />

Codes kontrollieren, Rahmenbedingungen für die<br />

Serialisierung setzen usw..<br />

Kontrolliert den Zugriff auf verschiedene Teile der<br />

Benutzeroberfläche. Unter anderem geht es hier um<br />

den Einsatz von Fenstern, die Verwendung der Zwischenablage<br />

und die Reaktion auf Ereignisse.<br />

Code-Identität<br />

Mit der Identität des Codes verbundene Rechte können nicht explizit angefordert<br />

werden – sie ergeben sich implizit. Eine entsprechende Signatur des Codes gibt<br />

dem NGWS-Laufzeitsystem die Möglichkeit, unter anderem den Ursprung (wie<br />

etwa eine Website) sowie den Hersteller herauszufinden.


202<br />

Rollenbasierte Sicherheit<br />

Tabelle 12.2 gibt eine kurze Beschreibung der in diesem Zusammenhang verwendeten<br />

Rechte.<br />

Recht<br />

PublisherIdentityPermission<br />

StrongNameIdentityPermission<br />

ZoneIdentityPermission<br />

SiteIdentityPermission<br />

URLIdentityPermission<br />

Beschreibung<br />

die Signatur einer NGWS-Komponente. Sie stellt<br />

sozusagen die Unterschrift des Herstellers dar.<br />

gibt den den stark verschlüsselten Namen der<br />

Komponente an. Die Identität des Codes setzt<br />

sich aus dem stark verschlüsselten Namen und<br />

dem regulären Namen der Komponente zusammen.<br />

gibt die Zone an, aus der der Code stammt. (Jeder<br />

URL kann zu jedem Zeitpunkt nur einer Zone<br />

angehören.)<br />

gibt die Website an, von der der Code stammt<br />

gibt den URL an, von dem der Code stammt<br />

Tab. 12.2: Mit der Identität des Codes verbundene Rechte<br />

12.2 Rollenbasierte Sicherheit<br />

Auch wenn Ihnen Sicherheit auf der Basis individueller Benutzerrechte von COM<br />

her ein Begriff sein wird und sich das Sicherheitsmodell von NGWS in diesem<br />

Punkt kaum unterscheidet, sollten Sie diesen Abschnitt nicht überspringen:<br />

Einige (wenige) Dinge gibt es nämlich schon, die Ihre Beachtung verdienen.<br />

Die rollenbasierte Sicherheit von NGWS baut auf einen sogenannten Prinzipal<br />

auf, der entweder einen Benutzer oder einen im Namen des Benutzers handelnden<br />

Agenten repräsentiert. NGWS-Anwendungen führen Sicherheitsprüfungen entweder<br />

auf Basis der Identität des Prinzipals oder seiner Rollenzugehörigkeit aus.<br />

Was unter einer Rolle zu verstehen ist, lässt sich am einfachsten anhand eines Beispiels<br />

erklären. Nehmen wir also eine Bank mit ihren Angestellten, einigen Prokuristen<br />

und dem Filialleiter. Ein Angestellter kann ein Formular für einen Kreditantrag<br />

vorbereiten – unterschreiben muss es aber ein Prokurist. Welcher der<br />

Prokuristen diese Unterschrift leistet (oder ob es gleich der Filialleiter tut), ist<br />

egal: Es kommt darauf an, dass irgendjemand aus der Gruppe der Prokuristen zum<br />

Stift greift.<br />

In einem etwas mehr technischen Kontext ist eine Rolle eine benannte Gruppe<br />

von Benutzern, die identische Rechte haben. Ein Prinzipal kann Mitglied mehrerer<br />

Rollen (also Benutzergruppen) sein. Ob bestimmte Aktionen in seinem<br />

Namen ausführbar sind oder nicht, ergibt sich aus der Rollenzugehörigkeit.


Kapitel 12 • Sicherheit 203<br />

Wie zuvor schon einmal kurz angedeutet, muss es sich bei einem Prinzipal nicht<br />

unbedingt um einen Benutzer handeln – er kann auch ein Agent sein. Tatsächlich<br />

gibt es insgesamt drei Arten von Prinzipalen:<br />

• Generische Prinzipale – repräsentieren Benutzer ohne Authentifizierung und<br />

die für sie erlaubten Rollen.<br />

• Windows-Prinzipale – bilden Windows-Benutzer und deren Gruppen (Rollen)<br />

ab. Identitätswechsel (also der Zugriff auf Ressourcen im Namen eines anderen<br />

Benutzers) wird unterstützt.<br />

• Selbstdefinierte Prinzipale – werden von Anwendungen angelegt und können<br />

den Identitätsbegriiff sowie die angenommenen Rollen bei Bedarf erweitern.<br />

Das setzt allerdings voraus, dass Ihre Anwendung für eine Authentifizierung<br />

sorgt und die Typen bereitstellt, die den Prinzipal implementieren.<br />

Wie das in der Praxis aussieht? Nun, das NGWS-Subsystem definiert eine Klasse<br />

PrincipalPermission, die für die Konsistenz mit Codezugriffsrechten sorgt. Diese<br />

Klasse macht es dem Laufzeitsystem möglich, Autorisierungen in ähnlicher<br />

Weise wie Codezugriffsrechte zu prüfen und stellt Ihrem Code darüber hinaus<br />

Informationen zur Identität des Prinzipals und seiner Rollen zur Verfügung.<br />

12.3 Zusammenfassung<br />

Dieses letzte Kapitel hat die Sicherheitskonzepte vorgestellt, die im NGWS-Subsystem<br />

zu finden sind. Codezugriffsrechte lassen sich in aktiv angeforderte Standardrechte<br />

und Sicherheitseinstufungen aufgrund der Code-Identität (Quelle,<br />

Hersteller, etc.) unterteilen. Die rollenbasierte Sicherheit sorgt dafür, dass ein<br />

Benutzer bzw. ein ihn vertretender Agent auf die Operationen beschränkt ist, die<br />

dieser Benutzer auf dem jeweiligen System ausführen kann.

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!