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.