C#
SYBEX-WebBook®
C# Otmar Ganahl
Z
Der Verlag hat alle Sorgfalt walten lassen, um vollständige und akkurate Infor...
115 downloads
1462 Views
6MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
C#
SYBEX-WebBook®
C# Otmar Ganahl
Z
Der Verlag hat alle Sorgfalt walten lassen, um vollständige und akkurate Informationen in diesem Buch bzw. Programm und anderen evtl. beiliegenden Informationsträgern zu publizieren. SYBEX-Verlag GmbH, Düsseldorf, übernimmt weder die Garantie noch die juristische Verantwortung oder irgendeine Haftung für die Nutzung dieser Informationen, für deren Wirtschaftlichkeit oder fehlerfreie Funktion für einen bestimmten Zweck. Ferner kann der Verlag für Schäden, die auf eine Fehlfunktion von Programmen, Schaltplänen o.Ä. zurückzuführen sind, nicht haftbar gemacht werden, auch nicht für die Verletzung von Patent- und anderen Rechten Dritter, die daraus resultiert. Projektmanagement: Anita Kucznierz DTP: Renate Felmet-Starke, Willich Endkontrolle: Mathias Kaiser Redaktionsbüro, Düsseldorf Umschlaggestaltung: Guido Krüsselsberg, Düsseldorf Farbreproduktionen: Fischer GmbH, Willich Belichtung, Druck und buchbinderische Verarbeitung: Bercker Graphischer Betrieb, Kevelaer
ISBN 3-8155-0125-3 1. Auflage 2002 Dieses Buch ist keine Original-Dokumentation zur Software der Firma Microsoft. Sollte Ihnen dieses Buch dennoch anstelle der Original-Dokumentation zusammen mit Disketten verkauft worden sein, welche die entsprechende Microsoft-Software enthalten, so handelt es sich wahrscheinlich um Raubkopien der Software. Benachrichtigen Sie in diesem Fall umgehend Microsoft GmbH, Edisonstr. 1, 85716 Unterschleißheim – auch die Benutzung einer Raubkopie kann strafbar sein. Der Verlag und Microsoft GmbH. Alle Rechte vorbehalten. Kein Teil des Werkes darf in irgendeiner Form (Druck, Fotokopie, Mikrofilm oder in einem anderen Verfahren) ohne schriftliche Genehmigung des Verlages reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Printed in Germany Copyright © 2002 by SYBEX-Verlag GmbH, Düsseldorf
Inhaltsverzeichnis 5
Inhaltsverzeichnis Vorwort Widmung Danksagung
15 17 19
Kapitel 1
Einleitung
21
Kapitel 2
C# – die neue Programmiersprache
27
C#-Einstieg
28
Einsprungsfunktion
32
Namensraum
34
Der Build-Prozess
35
Schutzklassen-Modifizierer
41
Zusammenfassung
42
Klassen und Objekte unter C# Klassen Grunddatentypen
42 43 49
Schlüsselwort using System.Object
50 51
Strukturen vs. Klassen Boxing und Unboxing
53 57
Enumerationen
58
Methoden Methode mit Bindung
60 60
Statische Methode Überladen von Methoden
61 61
Überladen von Operatoren
62
ref- und out-Parameter
64
Eigenschaften (Properties)
67
Ausnahmeverarbeitung – Exception Handling (EH) try – catch – throw
69 71
finally
75
Performanz
76
C# 6 Vererbung
82 84
Schutzkonzept
85
Interface (Schnittstellen)
86
Zusammenfassung
88
Felder und Collections
89
Felder aus Wertinstanzen
89
Felder aus Referenzinstanzen System.Array
91 92
Indexer
93
Die Schnittstelle IEnumerable und IEnumerate ArrayList
94 96
Maps
97
Kontrollstrukturen
98
Verzweigungen
98
Schleifen und Iterationen Sprungbefehle
100 100
Delegates Multicast-Delegates
101 104
Events
106
System.EventHandler – Delegate
Kapitel 3
77
Virtuelle Methoden Abstrakte Basisklassen
110
Attribute
112
Zusammenfassung
117
Baugruppen (Assemblies)
119
Einleitung
120
Grundlagen
121
Singlefile- und Multifile-Assemblies
123
Singlefile-Assembly
123
Multifile-Assembly
125
ILDASM (IL Disassembler)
128
Assembly-Entwicklung unter Visual Studio.NET
131
Inhaltsverzeichnis 7
Kapitel 4
Shared Assembly
135
Zusammenfassung
137
XML-Einführung
139
Einleitung
140
XML-Grundlagen
140
Das XML-Informationsmodell Verarbeitungsanweisungen
141 144
Elemente Attribute
144 145
Kommentare
146
Zeichenreferenzen Zeichenfolgen (Character Data)
147 147
Schemas
147
XPATH
148
Kurzformen
Kapitel 5
150
XSLT
150
XML-Klassen
153
Einleitung
154
XML-Dateien schreiben
156
XML-Dateien lesen XmlTextReader
160 160
XmlValidatingReader DOM-Objektmodell
163 165
XmlNode
166
XML-Dokumente verändern
169
Suchen innerhalb von XML-Dokumenten
170
Zusammenfassung
172
XPATH
172
XSLT (XSL-Transformationen)
173
XML-Serialisierung von .NET-Objekten
175
Serialisierung von einfachen Objekten
175
C# 8
Kapitel 6
Serialisieren von komplexeren Datentypen
177
Serialisieren von Listen Namensgebung der Attribute
179 181
Zusammenfassung
182
Windows-Applikationen
185
Einführung
186
Grundlegende Architektur eines Win32-Windowsprogramms
188
.NET-Programmiermodell Die Klasse Form
190 194
Auf Fensterereignisse reagieren Steuerelement im Fenster
196 197
Zusammenfassung
200
Windows-Steuerelemente TextBox, TrackBar, RadioButton, Label Zusammenfassung Designer (Entwurfsansicht) Zusammenfassung Menü Kontextmenu
200 201 209 209 218 219 221
Werkzeugleiste
222
Dialoge
225
Kundenspezifische Steuerelemente Erweitern eines Steuerelements
230 230
Entwicklung eines neuen Steuerelements Zusammenfassung GDI+
233 236 236
Pen und Brush Font
237 237
Point, PointF
238
Rectangle, RectangleF
238
Size, SizeF
238
Die Klasse Graphics
238
Inhaltsverzeichnis 9 OnPaint-Methode
238
Koordinatensysteme Bilder, Bitmaps und Images
241 246
Ressourcen Erstellung von Dateien vom Typ .resource
Kapitel 7
Verwenden von Ressource-Dateien
251
Ressource-Datei über .resx-Dateien erstellen Zusammenfassung
254 256
Unterstützung der Kulturen Zusammenfassung
257 264
Konfigurationsdateien
265
Konfigurationen
266
.NET-Konfigurationsdateien
268
Format der .NET-Konfigurationsdateien XML-Konfigurations-Bearbeiter machine.config appSettings Simple-Email-Beispiel
Kapitel 8
248 249
268 269 273 275 277
Zusammenfassung
281
ADO.NET
283
Einleitung
284
Übersicht der Klassen
286
Klassen im Namensraum System.OleDb
286
Klassen für den Zugriff auf SQL-Server von Microsoft
286
Die Klasse DataSet
287
Zugriff auf MS SQL Server
287
Datenbank anlegen
287
Daten in die Tabelle eintragen
290
Eintrag löschen
294
Update eines Eintrages Daten lesen
295 296
Zusammenfassung
300
C# 10 DataSet
Kapitel 9
300
Manipulation von Daten innerhalb eines DataSets DataSet und XML
303 306
Datenbank aktualisieren mit Daten eines DataSets
309
DataGrid
313
Zusammenfassung
315
ASP.NET
317
Einleitung
318
ASP-Active Server Pages
319
Diskussion Von ASP zu ASP.NET
324 325
Web-Steuerelemente
326
Trennung zwischen Sicht und Funktion Verwendung von kompiliertem Code
328 331
Zusammenfassung
332
ASP.NET-Anwendungen mit VS Studio entwickeln Debugging von ASP.NET-Anwendungen Zusammenfassung Web-Steuerelemente
333 341 341 341
Gemeinsame Eigenschaften
341
HyperLink-Steuerelement Image-Steuerelement
343 343
CheckBox-Steuerelement
344
RadiobuttonList
345
ListBox
347
Zusammenfassung
348
Kundenspezifische Steuerelemente
348
Basisklasse Control
348
Kundenspezifische Attribute
350
Eingebettete Objekte
354
Die Attribut-Klasse Style Basisklasse WebControl
356 357
Composite Controls
358
Inhaltsverzeichnis 11 Statusverwaltung unter ASP.NET Application und Session Cache
Kapitel 10
362 366
Zusammenfassung
372
Web-Services
373
Einleitung
374
Ein einfaches Web-Service-Beispiel
374
MyWebServices
375
Web-Service im Web-Browser Client-Programme zu MathWebService
376 379
Entwicklung und Anwendung von Web-Services unter Visual Studio.NET
Kapitel 11
360
381
Web-Service-Anwendung
382
Client-Anwendungen
384
Zusammenfassung
388
Remoting unter .NET
391
Einleitung
392
Remoting-Infrastruktur
393
Client-aktivierte Objekte
395
FolderFileEnum-Klasse Server
395 398
Client
400
Server-aktivierte Objekte
403
Erzeugung von Remote-Objekten mit dem new-Operator
406
Channel
407
Lebensdauer (Leased Based Lifetime)
408
Die Schnittstelle ILease LifetimeServices Sponsor Konfigurieren des Servers und des Clients über Konfigurationsdateien
408 410 412 414
C# 12
Kapitel 12
Server-Konfiguration
414
Client-Konfiguration
416
IIS Hosting
418
Asynchrone Aufrufe
421
Events Remote-fähiger Objekte
426
Übergabe von Objekten in Remote-Methoden
426
Zusammenfassung
427
.NET-Klassen
429
Einleitung
430
Die Klasse System.Object ReferenceEquals
431 431
Equals
432
GetHashCode() ToString()
433 434
GetType()
434
Finalize()
434
Die Klasse System.String Split Die Klasse System.IO.Path Die Klasse Path
Kapitel 13
435 435 436 437
Die Klassen DirectoryInfo,FileInfo
439
Die Klasse WebClient
440
Ausgewählte C#-Kapitel
443
Interoperabilität mit COM
444
Einbinden von ActiveX-Steuerelementen
444
Zusammenfassung
448
Casting Explizites und implizites Casting Schlüsselwort explicit Schlüsselwort implicit C#-Präprozessor
448 448 451 451 452
Inhaltsverzeichnis 13
Kapitel 14
#define und #undef
452
#if, #elif, #else, #endif #region, #endregion
452 453
#warning, #error
454
Fallbeispiel WWWPhotoPool 455 Beschreibung und Architektur der WWWPhotoPool-Applikation
456
WWWPhotoPool-Server PhotoWebServer
456 458
Publisher
459
CD-Beispiel WWWPhotoPool-Server
459 459
Publisher
460
PhotoWeb
460
Stichwortverzeichnis
461
15
Vorwort Microsoft bietet mit .NET den Entwicklern eine Plattform an, in der sich die Grenzen zwischen World Wide Web und den Betriebssystemen verwischen. Die „natürliche“ .NET-Sprache stellt C# (C Sharp) dar. Als ein Vertreter der C-Sprachenfamilie besticht C# durch ihre Einfachheit und Leistungsfähigkeit. Es war für mich eine neue und schöne Erfahrung, ein Fachbuch zu C# und .NET zu schreiben. Es würde mich freuen, wenn Sie nach Lektüre dieses Buches mit vielen neuen Ideen und hoch motiviert Ihre Projekte in C# und .NET umsetzen.
17
Widmung Dieses Buch ist meiner Familie gewidmet.
19
Danksagung Während der Erstellung des Buches haben mich viele Personen unterstützt. Ein herzliches Dankeschön möchte ich stellvertretend aussprechen: Dem gesamten SYBEX-Team, besonders den Projektmanagerinnen Anita Kucznierz und Katja Roth, dem Fachlektor Ron Nanko, der Sprachlektorin Brigitte Hamerski und Ute Dick. Meinem Bruder Claudio Ganahl, der mich in vielen fachlichen und inhaltlichen Fragen unterstützte. Meiner Frau Maria und meinen Kindern Janine und Manuel für das Verständnis, dass Papa in letzter Zeit weniger Zeit für sie hatte. Blons, März 2002 Otmar Ganahl
Einleitung
C# 22
1 Die Sprache C# (gesprochen c-sharp) kann nicht getrennt von der Laufzeitumgebung .NET betrachtet werden. Mit .NET ist Microsofts Vision einer umfassenden Plattform umschrieben. Die herkömmlichen Programmiermodelle von Microsoft konnten den Entwicklungen in der IT-Branche der letzten Jahren nur mehr bedingt Rechnung tragen. Neue Strömungen, vor allem im Bereich des Internets, wurden in bestehende Programmiermodelle hinein gepfercht. In Folge wurden diese immer komplexer und der Aufwand, diese zu beherrschen, wurde immer größer. Großartige Entwicklungswerkzeuge haben diese Problematik zwar entschärft, aber nicht gelöst. Mit .NET versucht Microsoft sozusagen eine „Bereinigung“ des Zustandes zu erreichen. .NET definiert ein gänzlich neues Programmiermodell und berücksichtigt hier sämtliche Entwicklungen in der IT-Branche, angefangen von Internet, Sicherheit, Datenbankzugriffe, Webservices, XML, usw. bis hin zu den jüngsten Entwicklungen im Bereich des Software Engineerings. Sämtliche Erfahrungen der komponentenbasierenden Softwareentwicklung (COM) und der verteilten Programmierung (DCOM) sind in .NET eingeflossen. Konzepte, die mit der Zeit unübersichtlich und damit fehleranfällig wurden (Stichwort DLL-Hell, Registrierungsdatenbank, Installationsmechanismen usw.), sind unter .NET entsprechend neu überdacht worden und erzeugen nicht mehr diese Probleme, deren Behandlung in der Praxis viel Zeit kostete. Interessant ist auch der Ansatz, dass .NET als Softwareschicht zwischen Betriebssystem und den Applikationen fungiert, und damit ein Betriebssystem abstrahiert. Technisch spricht nichts dagegen, dass .NET-Runtimes auch für andere Betriebssysteme entwickelt werden, was zur Folge hätte, dass Programme auch über verschiedene Plattformen hinweg lauffähig wären. Durch die Einführung einer „Metasprache“ werden Applikationen sogar auch binär kompatibel. Das Ergebnis dieser Anstrengungen ist ein schlankes, einfaches, aber umso mächtigeres Programmiermodell, das leicht erlernbar und anwendbar ist. „Software as a service“ ist ein vielfach verwendetes MarketingSchlagwort von Microsoft im Zusammenhang mit dieser Technologie.
Einleitung 1 Mit .NET sind aber Anpassungen der Programmiersprachen notwendig. Die meisten „herkömmlichen“ Programmiersprachen (wie C, C++, VB) können oft syntaktisch die neuen Features des .NET Programmiermodells nicht unterstützen (z.B. Garbage Collecting). Da die C-Sprachen vor allem unter professionellen Entwicklern populär sind, hat Microsoft eine neue Sprache, C# (C-Sharp), entwickelt. Schon aus dem Namen der neuen Programmiersprache geht hervor, dass C# ein Vertreter der C-Sprachenfamilie darstellt. Und in der Tat sind viele Konzepte, allen voran aus C++ und Java (Java wird ebenfalls in die Kategorie C-Sprachen eingeordnet), in C# wiederzufinden. Die Mächtigkeit der C-Sprachen wurde mit der Einfachheit moderner Sprachansätze kombiniert, was auch sehr gut gelungen ist. Die Ähnlichkeit mit Java ist unverkennbar. Das vorliegende Buch ist vor allem für Softwareentwickler und Programmierer unter Microsoft Betriebssystemen gedacht, die einen schnellen und kompakten Einstieg in die Thematik C# und .NET wünschen. Notwendig sind dazu Grundkenntnisse der objektorientierten Softwareentwicklung. Sollten Sie C++ oder Java beherrschen, dann werden Sie sich beim Erlernen der Grundlagen von C# sehr wohl fühlen. Aber auch Visual BasicProgrammierer sollten sich mit der vorliegenden Lektüre nicht schwer tun. Es wird ebenfalls davon ausgegangen, dass Sie Visual Studio 6.0 kennen und „Debugger“ für Sie kein Fremdwort darstellt. An dieser Stelle wird auch explizit darauf hingewiesen, dass dieses Buch kein Referenzbuch der Sprache C# darstellt. Nach Durcharbeit dieser Lektüre sollten Sie ein fundiertes C#- und .NET-Wissen besitzen, und damit die Voraussetzung geschaffen haben, um sich über begleitende Technologiebeobachtung zum professionellen .NET-Programmierer zu entwickeln. Bestimmte Kapitel benötigen allerdings einen höheren Wissensstand, so z.B. Datenbankenprogrammierung, XML. Hier wird auf einschlägige Literatur verwiesen. Nachfolgend werden noch einige Hinweise zum Gebrauch dieses Buches gegeben. Ziel ist es, Sie in möglichst kurzer Zeit zu befähigen, professionellen .NET-Code unter C# zu produzieren. Dies geht natürlich nicht von heute auf morgen. Damit die
23
C# 24
1 Lernkurve aber trotzdem kurz und steil verläuft, hier einige Tipps und Anmerkungen. 1
Lesen Sie ein Kapitel oder einen Abschnitt zuerst in Ruhe und gemütlicher Umgebung durch. Erst dann begeben Sie sich „an die Tasten“. Sie werden beim erstmaligen Durchlesen sicherlich nicht alles verstehen, aber Sie bekommen einen Überblick, was Sie in diesem Kapitel erwartet.
2
Kodieren Sie so gut wie möglich jedes Beispiel selbst aus. Sie werden vielleicht einwenden, dass Sie C# und .NET erlernen wollen und nicht Maschinenschreiben. Unterschätzen Sie aber nicht das daraus entstehende Lernpotenzial. Jeder Tippfehler hat einen KompilerFehler zur Folge, den Sie analysieren und erkennen müssen. Außerdem lernen Sie das Entwicklungssystem intuitiv, sozusagen nebenbei kennen. Das Beherrschen der Features des Entwicklungssystems ist eine wesentliche Voraussetzung für die spätere Produktivität beim Erzeugen von Code.
3
Gerade beim Erlernen von Konzepten ist es besser kleine und überschaubare Beispiele zu verwenden, die den Blick speziell auf die gerade behandelte Thematik beschränken. Oft wird der Fehler gemacht, in ein Beispielprogramm auch mehr oder weniger komplexe Algorithmen und spitzfindige Softwaremuster zu verpacken. Mit diesen können Sie sich dann beschäftigen, wenn Sie die Sprache beherrschen. Daher werden vor allem die ersten Beispiele keinen Anspruch auf sinnvolle Verwendbarkeit erheben. Es sind einfache Beispiele, die sich in der Didaktik begründen.
4
Experimentieren Sie! Variieren Sie Codeabschnitte, erzeugen Sie künstlich Kompiler-Fehler und testen Sie ständig, ob Ihr Denkmodell schlüssig ist.
5
Abonnieren Sie eine Fachzeitschrift zum Thema und betreiben Sie aktiv Technologiebeobachtung!
Sämtliche Beispiele basieren auf Visual Studio.NET (deutsche Ausgabe – April 2002). Wenn Sie Visual Studio.NET auf Ihrem Rechner installieren, dann benötigen Sie ca. 2GByte Festplat-
Einleitung 1 tenspeicher. Auf Ihrem Entwicklungsrechner sollten Sie auch den IIS (Internet Information Server) und SQL-Server (oder aber MSDE) installiert haben. Wenn Sie die Installation noch nicht durchgeführt haben, dann installieren Sie unbedingt zuerst den IIS und erst dann das .NET-Framework mit Visual Studio.NET. Es wird auch ausreichend RAM-Speicher (wenigstens 128, besser 256 MB) empfohlen, da ansonsten das Entwickeln zu einem Geduldsspiel wird. Ein Internetanschluss wird vorausgesetzt. Für Kapitel 11: Remoting unter .NET ist das Vorhandensein eines kleinen TCP-Netzwerkes von Vorteil. Installieren Sie auch die aktuellste Version der MSDN (Microsoft developer network). Es wird davon ausgegangen, dass Sie diese umfangreiche Wissensdatenbank ausgiebig nutzen werden (und auch müssen). Auf der Begleit-CD zu diesem Buch sind sämtliche Beispiele in auskodierter Form zu finden. Vielfach werden Sie eingedeutschte Begriffe (z.B. Assemblies) oder „Mischwesen“ aus Deutsch und Englisch, wie „Member“Variable finden. Um die Lesbarkeit zu fördern, sind diese Wörter im Buch kursiv formatiert. Für konstruktive Kritik, Fehlerhinweise und Anregungen bin ich sehr dankbar. Zum Abschluss wünsche ich Ihnen viel Spaß mit diesem Buch. Sollte es Ihnen gefallen, empfehlen Sie es weiter. Blons im Biosphärenpark Großes Walsertal/Österreich im März 2002 Otmar Ganahl
25
C# – die neue Programmiersprache
C#-Einstieg
28
Klassen und Objekte unter C#
42
Grunddatentypen
49
Strukturen vs. Klassen
53
Enumerationen
58
Methoden
60
Eigenschaften (Properties)
67
Ausnahmeverarbeitung – Exception Handling (EH)
69
Vererbung
77
Interface (Schnittstellen)
86
Felder und Collections
89
Kontrollstrukturen
98
Delegates
101
Events
106
Attribute
112
Zusammenfassung
117
C# 28
2 In diesem Kapitel werden Sie in kleinen, überschaubaren Beispielen die Konzepte der Sprache C# experimentell kennen lernen. All diese Beispiele dienen einem rein didaktischen Zweck und erheben keinen Anspruch auf eine andere sinnvolle Verwendbarkeit. Aus der langjähriger Seminarpraxis ist dem Autor bekannt, dass dies sehr sinnvoll ist und die Lernkurve beträchtlich verkürzt. Sämtliche Beispiele werden Sie mit der Entwicklungsumgebung Visual Studio.NET durchführen. Es wird angenommen, dass Sie mit einer der Entwicklungsumgebungen von Visual Studio 6 vertraut sind, und es wird daher so weit wie möglich auf redundante Screenshots verzichtet. Das ist möglich, da eine gewisse Ähnlichkeit zu Visual Studio 6 vorhanden ist, und viele Features intuitiv erlernbar sind. Es wird vorausgesetzt, dass Sie die wichtigsten Konzepte der Sprachen C und C++ beherrschen.
C#-Einstieg In diesem ersten Beispiel wird eine ganz normale Textausgabe auf die Konsole erzeugt, wie es schon Kerninghan und Ritchie Mitte der 70er Jahren in Ihrem Klassiker C Programming getan haben. Es wird auf der Konsole derselben „ehrwürdige“ Text ausgegeben, wie damals Kernighan und Ritchie, nämlich „Hello World“. Starten Sie das Entwicklungssystem Visual Studio.NET und legen Sie zunächst eine Projektmappe an. Eine Projektmappe ist vergleichbar mit einem Workspace unter Visual Studio 6, erlaubt also mehrere Projekte unter einem Namen zu organisieren. Nennen Sie diese Projektmappe „DotNetExperiments“. Eine „leere“ Projektmappe legen Sie über das Menü Datei > Neu > Leere Projektmappe an. Ein Blick auf die erzeugte Dateistruktur im Datei-Explorer zeigt, dass im gewählten Zielordner ein Ordner namens DotNetExperiments angelegt wurde. In der Entwicklungsumgebung sollten Sie im Projektmappen-Explorer nun die neu angelegte Projektmappe sehen (wenn dieser nicht sichtbar ist, dann können Sie diesen mittels Menü Ansicht > Projektmappen-Explorer andocken).
C # – die neue Programmiersprache 2
29
Projektmappe erzeugen Abb. 2.1
Fügen Sie nun dieser Projektmappe ein erstes Projekt hinzu. Der Menübefehl Datei > Neu > Projekt öffnet hierzu einen Dialog. Markieren Sie Visual C#-Projekte, wählen Sie vorerst aus didaktischen Gründen ein Leeres Projekt aus, und geben Sie dann dem Projekt den Namen „HelloWorld“. Vergessen Sie aber nicht, die Option Zu Projektmappe hinzufügen zu aktivieren, da ansonsten eine neue Projektmappe erzeugt wird, die den Namen des Projektes hat (Sie kennen sicherlich das ähnliche Verhalten unter Visual Studio 6). Ein erneuter Blick auf die Dateistruktur zeigt, dass innerhalb des Projektmappen-Ordners ein neuer Ordner „HelloWorld“ angelegt wurde. Der Projektmappen-Explorer zeigt nun auch das Projekt an. Da das Projekt noch leer ist, fügen Sie diesem eine C#-Quelldatei hinzu. Dies geschieht mit dem Menübefehl Projekt > Neues Element hinzufügen. Im nun erscheinenden Dialog wählen Sie den Typ C# Codedatei. Geben Sie der Datei einen selbstsprechenden Namen. Statt des vom Entwicklungssystem vordefinierten Namens CodeFile1.cs können Sie Application.cs nehmen. Geben Sie nun folgenden Code ein. Eine genaue Diskussion erfolgt später, zuerst lernen Sie die Handhabung des Entwicklungssystems kennen.
C# 30
2
Projekt anlegen Abb. 2.2
Codedatei hinzufügen Abb. 2.3
CD-Beispiel HelloWorld1
class App //Definition einer neuen Klasse { //die Einstiegsfunktion public static void Main() {
C # – die neue Programmiersprache 2 System.Console.WriteLine("Hello World"); } }
/*Kein Strichpunkt notwendig*/
Nach einer Übernahme des abgedruckten Quelltextsegments können Sie mittels S + % den Build-Prozess und die anschließende Ausführung starten. Noch einige Bemerkungen zum Entwicklungssystem: Wenn Sie die Ordner-Struktur auf dem Dateisystem betrachten, werden Sie feststellen, dass im Projektmappen-Ordner für das neue Projekt ein eigener Ordner angelegt wurde. Dieser enthält unter anderem die Quelldatei Application.cs und einen weiteren Ordner bin. Hier befindet sich der Ordner Debug mit dem eigentlichen Programm HelloWorld.exe. Diese ausführbare Datei beinhaltet allerdings noch Debug-Informationen. Für die Erzeugung einer Release-Version schalten Sie die aktive Konfiguration mit dem Menübefehl Erstellen > KonfigurationsManager auf Release um. Wenn Sie das Projekt nun neu entwickeln, entsteht im Ordner Release eine ausführbare Datei ohne Debug-Informationen. Diese Datei ist auch deutlich kleiner und schneller.
HINWEIS Debug- und Releaseversionen In der Regel wird es so sein, dass Sie bis zur Fertigstellung eines Projekts ausschließlich mit Debugversionen arbeiten, die weiterführende Informationen enthalten, um mithilfe des Visual Studio Debuggers Fehlererkennung, -suche und -behebung durchführen zu können. Releaseversionen haben den Vorteil, diese für den Endanwender uninteressanten Daten nicht zu enthalten, was sich in der Größe der entstehenden Dateien und nicht zuletzt auch der Abarbeitungsgeschwindigkeit niederschlägt.
Natürlich können Sie das Kompilieren auch explizit über den Menüpunkt Erstellen > Erstellen bzw. Erstellen > Alles neu erstellen durchführen. Dasselbe gilt für das Starten des Programms aus der Entwicklungsumgebung heraus. Im Menüpunkt De-
31
C# 32
2 buggen sehen Sie die Möglichkeiten. Sämtliche Funktionalität wird auch direkt über die Werkzeugleisten angeboten.
Einsprungsfunktion Ein erfahrener C/C++-Programmierer wird bei diesem Beispiel nicht so schnell erschrecken. Was Kommentare anbelangt, so ist C- als auch C++-Stil möglich: //Definition einer neuen Klasse /*Kein Strichpunkt notwendig*/ Sie werden auch erkannt haben, dass mit dem Schlüsselwort class ein neuer Typ mit dem Namen App definiert wird. Dies funktioniert ähnlich wie bei C++, nur dass kein abschließender Strichpunkt am Blockende notwendig ist. Die Klasse App implementiert in diesem Beispiel nur eine statische Methode Main(). Sie werden hier die Einsprungsfunktion vermuten, und damit haben Sie auch Recht. Das wirft aber die Frage auf, warum Main() die Einsprungsfunktion darstellt, ist sie doch unter dem Namensraum einer Klasse definiert? Die Erklärung ist, dass es unter C# keine globalen Funktionen (und auch keine globalen Variablen) gibt, wie Sie es aus C oder C++ kennen. Sämtliche Funktionen müssen unter der Kontrolle einer Klasse sein. C++-Programmierer wissen auch, dass statische Methoden den globalen Funktionen aus C sehr ähnlich sind. Für den Aufruf von statischen Methoden sind unter C++ (wie auch unter C#) keine Instanzen der Klassen – also Objekte – notwendig, sondern nur die Angabe des Namensraumes. Wie findet aber der Programm-Loader nun diese Methode? In einem C#-Programm darf es nur genau eine Klasse mit einer statischen Methode Main() geben. Diese Methode gilt dann als Einstiegsfunktion. (Korrekterweise muss gesagt werden, dass sehr wohl mehrere statische Methoden mit dem Namen Main in unterschiedlichen Klassen vorkommen dürfen. In diesem Fall allerdings muss dem Kompiler explizit diejenige Klasse angegeben werden, dessen Main()-Funktion als Einsprungsfunktion dienen soll. Mehr dazu finden Sie in der MSDN). Die .NET-Laufzeitschicht sucht dann diese Methode in den ihr bekannten Klassen und ruft sie auf. Wie dies im Detail funktioniert, werden Sie später noch kennen lernen.
C # – die neue Programmiersprache 2 Experimentieren Sie, indem Sie eine weitere Klasse (mit Namen App1) definieren, die ebenfalls eine Methode Main() implementiert. Beim Kompilieren werden Sie eine entsprechende Fehlermeldung erhalten. class App //Definition einer neuen Klasse { //die Einstiegsfunktion public static void Main() { System.Console.WriteLine("Hello World"); } } /*Kein Strichpunkt notwendig*/ class App1 { public static void Main() { System.Console.WriteLine("Hello World – App1"); } } Benennen Sie nun die Methode Main() der Klasse App1 auf Main1() um – der Kompiler arbeitet nun, ohne einen Fehler auszugeben. Sie können diese Methode sogar jederzeit explizit aufrufen. class App { public static void Main() { System.Console.WriteLine("Hello World"); //Aufruf der statischen Methode Main1 aus App1 App1.Main1(); } } class App1 { public static void Main1() { System.Console.WriteLine("Hello World – App1"); } }
CD-Beispiel HelloWorld2
33
C# 34
2 Namensraum Um die Methode Main1() aufzurufen, müssen Sie Namensraum angeben. Wenn Sie in C++ eine Klasse deklarieren, entsteht automatisch auch ein Namensraum. C++ verwendet den zweifachen Doppelpunkt (scope operator), um den Namensraum anzugeben. Unter C# wird der Namensraum mit dem Punktoperator eingeleitet. (Verwechseln Sie App1 nicht mit einem Objekt, App1 ist der Name der Klasse!) App1.Main1(); Ein Namensraum kann unter C# auch mit dem Schlüsselwort namespace eingeführt werden. Im folgenden Codebeispiel wird die Klasse App1 innerhalb eines Namensraumes definiert. CD-Beispiel HelloWorld3
class App { public static void Main() { System.Console.WriteLine("Hello World"); //den Namespace angeben DotNetExperiments.App1.Main1(); } } //Erzeugung eines neuen Namensraumes namespace DotNetExperiments { class App1 { public static void Main1() { System.Console.WriteLine( "Hello World – App1"); } } } Sie sehen, dass nun die Angabe des Namensraumes DotNetExperiments notwendig ist, um dann über die Klasse App1 die statische Methode Main1() aufzurufen. DotNetExperiments.App1.Main1();
C # – die neue Programmiersprache 2
35
Der Punktoperator wird unter C# verwendet, um Namensräume zu öffnen und auf Klassen-Members zu verweisen (wie auch bei C++). An diesem Punkt dürfte es auch nicht schwer sein, die Programmzeile, die die Ausgabe auf die Konsole durchführt, zu verstehen. System.Console.WriteLine("Hello World"); System ist ein Namensraum in dem sich die Klasse Console befindet, die ihrerseits die statische Methode WriteLine() implementiert.
Der Build-Prozess Sie stellen sich jetzt sicherlich die Frage, wo denn die Klasse Console mit der statischen Funktion WriteLine.) definiert ist? Unter C++ würden Sie eine Bibliothek hinzulinken, die die Implementierungen der Funktionen der Klasse enthält – im Quellcode würden durch einen Verweis auf eine Header-Datei diese Funktionsprototypen vereinbart werden. Eine vergleichbare Vereinbarung der Klasse System.Console fehlt aber im vorliegenden Beispiel! Um diesen Vorgang zu verstehen, werfen Sie erst einen Blick auf den C/C++-Mechanismus.
C/C++-Mechanismus Abb. 2.4
C# 36
2 Um eine Objektdatei zu generieren, genügen dem Kompiler die Prototypen der Funktionen und Datentypen. Die eigentliche Implementierung fügt dann der Linker in Form einer statischen Objekt-Bibliothek oder einer Import-Bibliothek (für implizite Dll-Bindung) hinzu. Für C++-Programmierer ergibt sich ein erhöhter Aufwand dadurch, dass die konsistente Pflege von Header-Datei und Bibliothek notwendig ist. Im COM-Programmiermodell unter Windows (component object model) ist die Verwaltung auch nicht einfacher. Die Beschreibung von COM-Komponenten erfolgt in binärer Form in einer der Typbibliotheken, die bei modernen COM-Komponenten als Ressource direkt in den Komponenten integriert wird. Damit sind wenigstens sowohl die Beschreibungen als auch der Code in derselben Datei, was die Handhabung vereinfacht. Die Programmiersprache Visual Basic bedient sich intensiv dieser Typbibliotheken. In der Registrierung ist aber der Ort der Typbibliothek und der Ort des Binärcodes auf der Festplatte an unterschiedlichen Einträgen zu verwalten. Die Konsistenz der Eintragungen ist für das Funktionieren einer Komponente eine wesentliche Voraussetzung. Und genau dieser Umstand ist fehleranfällig. Unter Visual C++ können mittels Spracherweiterungen Header-Dateien aus den Typbibliotheken generiert werden. Visual Basic kann die Information direkt aus der Typbibliothek verwenden. .NET geht einen neuen und anderen Weg. Bei den herkömmlichen Programmiermodellen (Win32 und COM) sind Komponenten meistens in Dlls verpackt. Unter .NET wird eine Einheit, die Komponenten in binär ausführbaren Code enthält, Assembly genannt. Eine Assembly ist ein wohldefinierter Verbund von Dateien. Im einfachsten Fall besteht eine Assembly aus genau einer Dll. Wesentlich ist nun, dass die Beschreibung der Komponente direkt im Assembly, in Form von so genannten „Metadaten“, untergebracht ist. Damit eröffnen sich sehr viele Möglichkeiten. .NET-Sprachen und damit auch C# brauchen daher kein Header-Dateien, sondern nur die Angabe des Assemblies. In diesem findet sich alles: die Beschreibungen für den Kompilier-Vorgang und die Implementierungen für den Link-Vorgang.
C # – die neue Programmiersprache 2 Damit ist auch eine logische Trennung des Build-Prozesses in einen Kompilier- und Link-Vorgang nicht mehr notwendig, da sämtliche Informationen (Beschreibung und Implementierung) im Assembly zu finden sind. Der Aufruf aus der Kommandozeile würde sich wie folgt gestalten: csc /out:HelloWorld.exe /r:mscorlib.dll Application.cs csc.exe ist der C#-Kompiler. Über die Option /out: kann der Name der entstehenden exe-Datei angegeben werden. Sämtliche Assemblies, die für die Applikation verwendet werden, sind mit der Option /r: aufzulisten. Zu guter Letzt folgt die Aufzählung der Quellcode-Dateien. Mscorlib.dll ist die Kern-Assembly der Laufzeitschicht, und wird immer hinzugelinkt. Diese Kern-Assembly beinhaltet die Implementierungen der grundlegenden .NET-Klassen, unter anderem auch die Implementierung der Klasse Console. Bei der Verwendung des Entwicklungssystems wird dieser Aufruf natürlich implizit generiert. Überprüfen Sie diesen Sachverhalt. Erzeugen Sie in einem beliebigen Ordner mit einem beliebigen Editor (z.B. Notepad) eine Datei Application.cs mit obigem Code und geben Sie über die Konsole die Anweisung csc /out:HelloWorld.exe /r:mscorlib.dll Application.cs ein. Nach fehlerfreier Ausführung befindet sich die ausführbare .exe-Datei in diesem Ordner. Verwenden Sie die Konsole von Visual Studio.NET! Diese finden Sie unter Start > Programme > Microsoft Visual Studio.NET 7.0 > Visual Studio.NET Tools > Visual Studio.NET Command Prompt. Hier ein anderes, interessantes Beispiel, das die Leistungsfähigkeit der „mitgelieferten“ .NET-Klassen demonstriert, und auch gleichzeitig die Verwendung von Assemblies verdeutlicht. Das Beispielprogramm zeigt, wie eine E-Mail per Programm versendet werden kann. Für eine ordnungsgemäße Funktion dieses Programms ist es erforderlich, dass Ihr Rechner über einen Internetanschluss verfügt. Ist dieses nicht der Fall, würde die Programmausführung eine Exception auslösen – überspringen Sie unter diesen Umständen einfach das folgende Beispiel oder vollziehen es nur theoretisch nach.
CD-Beispiel HelloWorld4
37
C# 38
2
C#-Mechanismus Abb. 2.5
Erzeugen Sie ein neues C#-Projekt in der Projektmappe. Nennen Sie es „SmartEmail“ (Datei > Neu > Projekt – und vergessen Sie nicht die Option Zu Projektmappe hinzufügen zu aktivieren). Fügen Sie eine Quelldatei hinzu (App.cs) und tippen Sie den folgenden Code ein. Im Projektmappen-Explorer können Sie nun beide Projekte sehen. Das fett hervorgehobene Projekt ist das derzeit aktive. Sollte das neue erstellte Projekt nicht aktiv sein, dann holen Sie dies bitte nach, indem Sie das Projekt im Projektmappen-Explorer markieren und im Kontextmenü (rechte Maustaste) Als Startprojekt festlegen auswählen.
Im ProjektmappenExplorer können mehrere Projekte verwaltet werden Abb. 2.6
Im Menüpunkt Erstellen erscheinen nun die zusätzlichen Einträge Projektmappe erstellen bzw. Projektmappe neu erstellen. Damit können alle Projekte einer Solution in einem kompiliert werden.
C # – die neue Programmiersprache 2 class App { public static void Main() { string sAbsender; string sAdresse; string sBetreff; string sText; //Hier bitte Ihren(!) smtp – Server URL eingeben System.Web.Mail.SmtpMail.SmtpServer = "smtp.provider.xx"; System.Console.WriteLine("Kleiner E-Mail Sender"); System.Console.Write("Absender: "); sAbsender = System.Console.ReadLine(); System.Console.Write("E-Mail Adresse: "); sAdresse = System.Console.ReadLine(); System.Console.Write("Betreff: "); sBetreff = System.Console.ReadLine(); System.Console.Write("Text: "); sText = System.Console.ReadLine(); System.Console.Write("E-Mail versenden? (j/n)"); if(System.Console.ReadLine()== "j") { System.Console.WriteLine( "E-Mail wird übertragen..."); System.Web.Mail.SmtpMail.Send(sAbsender, sAdresse, sBetreff, sText); System.Console.WriteLine( "E-Mail wurde übertragen"); } else { System.Console.WriteLine( "E-Mail wurde nicht übertragen"); } } Wenn Sie nun den Build-Prozess starten, wird der Kompiler mit einer Fehlermeldung reagieren. Der Grund liegt darin, dass in diesem Beispiel der Datentyp System.Web.Mail.SmtpMail ver-
CD-Beispiel SmartEmail
39
C# 40
2 wendet wird, den der Kompiler aber nicht kennt, weil diese Klasse nicht in mscorlib.dll implementiert ist. Die Klasse findet sich im Assembly System.Web. Um dieses Programm aus der Konsole heraus zu entwickeln, müssen Sie folgende Kommandozeile eintippen. csc /out:smartemail.exe /r:mscorlib.dll /r:system.web.dll Application.cs Zusätzlich zur Kern-Assembly wird ein weiteres Assembly (system.web.dll) angegeben. Im Entwicklungssystem fügen Sie diese Information im Projektmappen-Explorer durch. Expandieren Sie, wenn notwendig im Projektmappen-Explorer den Baum des Projektes SmartEmail, markieren Sie den Eintrag Verweise und betätigen Sie den Menübefehl Verweis hinzufügen im Kontextmenü.
Hier werden sämtliche verfügbaren Assemblies aufgelistet Abb. 2.7
Selektieren Sie das Assembly System.Web.dll im Reiter .NET und bestätigen Sie. Nchfolgend sollte nun der Build-Prozess ohne Fehlermeldung des Kompilers ablaufen. Vergessen Sie nicht, einen gültigen DNS-Namen eines SMTPServers anzugeben – am besten den von Ihrem Provider (bei Outlook finden Sie den Namen in den Konto-Einstellungen).
C # – die neue Programmiersprache 2
41
Der in diesem Listing dargestellte Name ist natürlich nicht gültig. System.Web.Mail.SmtpMail.SmtpServer = "smtp.provider.xx"; Starten Sie nun das Programm, geben als Absender Ihren Namen an, eine gültige E-Mail-Adresse (nehmen Sie am besten Ihre eigene Adresse), einen Betreff (z.B. „Hello World modern“) und einen kleinen Text. Mit der Taste j wird nun eine E-Mail versendet. Das ist doch schon recht beeindruckend. Sie sehen, wie einfach diese Klasse zu verwenden ist. Sicherlich fallen Ihnen einige tolle Anwendungen ein, die E-Mails versenden (z.B. ein Überwachungsprogramm einer Anlage, die im Bedarfsfall ein EMail versendet, usw.). In der .NET-Klassenbibliothek wimmelt es nur so von solchen leistungsfähigen Klassen, die nur darauf warten, angewendet zu werden. Aber erst sollen Sie C# sicher beherrschen.
Schutzklassen-Modifizierer Unter C# ist bei der Definition jeder Methode und auch jeder Eigenschaft die explizite Angabe eines Schutzklassen-Modifizierers notwendig, der die jeweilige Schutzklasse angibt. Im Beispiel ist die Methode public definiert. Bei C++ wird mit dem Schlüsselwort public für einen Abschnitt in der Klassendeklaration die Schutzklasse definiert. Alle Methoden und Eigenschaften, die sich in diesem Abschnitt befinden, haben dann diese Schutzklasse. Die Schutzklassen werden in späteren Kapiteln noch genauer betrachtet. //Schutzklassenangabe unter C++ class MyClass { public: void Method1(); void Method2(); private: void PrivatMethod1(); };
Angabe der Schutzklasse bei C++
C# 42
2
Angabe der Schutzklasse unter C#
//Schutzklassenangabe unter C# public static void Main() //explizite Angabe { ... }
Zusammenfassung In diesem Abschnitt haben Sie einen ersten Eindruck vom Programmiermodell .NET bekommen, die Handhabung des Entwicklungssystems kennen gelernt und erste Betrachtungen über Konzepte wie Namensraum, Einsprungsfunktion, Assemblies und Metadaten durchgeführt. Sie werden nun im nächsten Abschnitt die objektorientierten Features der Sprache C# kennen lernen.
Klassen und Objekte unter C# Im vorherigen Kapitel haben Sie einen grundsätzlichen Einstieg in die Sprache C#, der .NET-Laufzeitumgebung und in die Handhabung des Entwicklungssystems Visual Studio.NET getätigt. In diesem und den nächsten Abschnitten werden Sie sich auf die typisch objektorientierten Features der Sprache C# und der .NET-Laufzeitumgebung konzentrieren. Wieder in Form von kleinen, überschaubaren Beispielen werden Sie nacheinander diese Konzepte kennen lernen. Kleine Beispiele eignen sich auch hervorragend zum Experimentieren. Nutzen Sie diese Gelegenheiten, versuchen Sie mit Experimenten die Richtigkeit Ihres Denkmodells zu prüfen und dieses gegebenenfalls zu korrigieren. Es wird davon ausgegangen, dass das Bruchrechnen für die meisten Leser kein Problem darstellen wird. In den nächsten Kapiteln werden Sie anhand einer Klasse, die Bruchzahlen modelliert, die objektorientierten Features kennen lernen. Sukzessive wird diese Klasse in den nächsten Kapiteln ausgebaut und mit neuen Features versetzt. Bevor Sie aber damit beginnen, noch einige Begriffsdefinitionen zu den Bruchzahlen. Bei einer Bruchzahl (engl. fraction), wie z.B. ¾ , wird der Wert über dem Bruchstrich „Zähler“ (engl. numerator) und der Wert unter dem Bruchstrich „Nenner“ (engl. denominator) genannt.
C # – die neue Programmiersprache 2 Klassen Für die nächsten Beispiele erzeugen Sie am besten eine neue Projektmappe FractionExamples. Fügen Sie in gleicher Weise ein neues C#-Projekt hinzu, wie Sie es im Einführungsbeispiel kennen gelernt haben. Das Projekt soll den Namen Fraction erhalten. (Vergessen Sie nicht die Option Zu Projektmappe hinzufügen zu aktivieren.) Anschließend erzeugen Sie eine C#Quelldatei (fraction.cs) und geben folgenden Code ein. namespace FractionExamples { class Fraction { public System.Int32 z; //Zähler public System.Int32 n; //Nenner public Fraction () { z = 0; n = 1; } public Fraction (System.Int32 z) { this.z = z; n = 1; } public Fraction ( System.Int32 z, System.Int32 n) { this.z = z; this. n = n; } public void WriteToConsole() { System.Console.WriteLine("{0}/{1}", z, n); } } }
CD-Beispiel Fraction1
43
C# 44
2 //Hauptprogramm für Testzwecke class App { public static void Main() { FractionExamples.Fraction r1 = new FractionExamples. Fraction (); FractionExamples. Fraction r2 = new FractionExamples. Fraction (3); FractionExamples. Fraction r3 = new FractionExamples. Fraction (6,5); r1.WriteToConsole(); r2.WriteToConsole(); r3.WriteToConsole(); } } Ihnen ist bereits die einzig statische Methode mit dem Namen Main() bekannt, die die Einsprungsmethode der Applikation darstellt. Die Klasse, die diese Methode hält, wurde hier beliebig App genannt. Der neue Datentyp wurde mit dem Schlüsselwort class in einem eigenen Namensraum definiert (FractionExamples) erzeugt. Der Datentyp erhielt den Namen Fraction. Die Klasse Fraction modelliert eine Bruchzahl mit einem Zähler und einem Nenner über die Member-Variablen z (Zähler) und n (Nenner). Hier wird für den Klassennamen die englische Bezeichnung und für die Member-Variablen die deutsche Bezeichnung verwendet. Dies ist zwar nicht konsequent, da aber im deutschen Sprachraum die Bezeichnungen numerator und denominaor vollkommen ungebräuchlich sind, ist dies gerechtfertigt. Ihnen ist sicherlich die ungewohnte Definition der Member-Variablen z und n aufgefallen. Im Namensraum System existieren die Grunddatentypen (primitive types). System.Int32 ist ein solcher Typ. Unter .NET und den .NET-Sprachen, sind also auch die Grunddatentypen Objekte (dies gilt ja bekanntlich nicht bei C++). public System.Int32 z; public System.Int32 n;
C # – die neue Programmiersprache 2 Weiter erkennt der gelernte C++-Programmierer drei Konstruktoren zur Initialisierung einer Variablen vom Typ Fraction einmal ohne, einmal mit einem und einmal mit zwei Initialisierungsparametern. Unter C++ könnte man mittels initialisierten Parametern den Codierungsaufwand verkürzen. Unter C# gibt es aber keine initialisierten Parameter und daher ist die explizite Codierung aller drei Konstruktoren notwendig. public Fraction(System.Int32 z, System.Int32 n) { this.z = z; this.n = n; } Interessant ist hier die Implementierung. Der Konstruktor verwendet als Parameternamen z und n, also dieselben Namen wie die der Member-Variablen. In der Implementierung überblenden diese lokalen Parametervariablen die Member-Variablen. Aber unter C# ist es möglich, mit dem Schlüsselwort this auf die Member-Variablen zu verweisen. Unter C++ ist this ein Zeiger und daher die Verwendung des Pfeiloperators notwendig, aber unter C# gibt es keine Syntax für Zeiger, daher kann und muss der Punktoperator verwendet werden. public void WriteToConsole() { System.Console.WriteLine("{0}/{1}",z,n); } Die Klasse Fraction implementiert auch eine Methode WriteToConsole() zur Ausgabe einer Bruchzahl. Eine Ausgabe auf die Konsole geschieht per WriteLine der Klasse Console im Namensraum System. Diese Methode kann ähnlich printf(...) eine variable Anzahl von Parametern aufnehmen. Ähnlich den Formatelementen unter C (%d, %f, etc.) können im Formatstring Platzhalter für die zu formatierenden Werte angegeben werden. Dies geschieht in Form eines nullbasierenden Index, der in geschweifter Klammer angegeben wird – {0} bezieht sich demnach auf den ersten spezifizierten Parameter, {1} auf den zweiten und so weiter. Nun aber zum Hauptprogramm.
45
C# 46
2 public static void Main() { FractionExamples.Fraction r1 = new FractionExamples. Fraction (); FractionExamples. Fraction r2 = new FractionExamples. Fraction (3); FractionExamples. Fraction r3 = new FractionExample. Fraction (6,5); r1.WriteToConsole(); r2.WriteToConsole(); r3.WriteToConsole(); } Dieser Teil ist auch für erfahrene C++-Programmierer erklärungsbedürftig, da hier doch neue, von C++ erheblich abweichende Konzepte verwendet werden. Sie müssen wissen, dass unter C# sämtliche Datentypen, die mit dem Schlüsselwort class definiert wurden, Referenzen darstellen. Mit der Anweisung FractionExamples.Fraction r1; wird nicht, wie Sie vielleicht vermuten, eine Variable angelegt, sondern nur Speicherplatz für eine Referenz auf r1. Wenn Sie jetzt an einen Zeiger denken, dann liegen Sie absolut nicht falsch. Es wird Speicherplatz auf dem Stack (!) für einen Zeiger auf ein Fraction-Element alloziert. Ein Fraction-Objekt existiert allerdings noch nicht! Ein Fraction-Element erzeugen Sie über den new Operator. Durch die Angabe der Konstruktorparameter wird dann der entsprechende Initialisierungscode verwendet. (Beachten Sie, dass auch für den Default-Konstruktor die Klammern verwendet werden müssen!). Das Objekt wird aber nicht auf dem Stack angelegt, sondern, wie auch bei C++ bei der Verwendung des new-Operators, auf dem Heap. Dieser Heap wird von der .NET-Umgebung verwaltet und ab sofort „managed heap“ genannt. C++-Programmierer sind gewohnt, mit new allozierte Speicherbereiche wieder freizugeben. Dies ist unter .NET nicht notwendig, weil diese Arbeit die .NET-Laufzeitumgebung übernimmt.
C # – die neue Programmiersprache 2
Referenzobjekt Abb. 2.8
Die .NET-Laufzeitumgebung hat einen „Garbage Collector“ (GC) implementiert, zu deutsch „Müllsammler“, der diese Arbeit übernimmt. Sie fragen sich nun vielleicht, wann dies geschehen wird. Dies kann deterministisch nicht vorausgesagt werden. Prinzipiell gilt: Wenn keine Referenz mehr auf ein Objekt existiert, darf der GC den Speicherplatz wieder freigeben. Im Beispiel kann der GC das Objekt auflösen, wenn sich der betreffende Stack-Bereich auflöst, oder im Programmierjargon, wenn der Scope (Gültigkeitsbereich) der Funktion (oder Methode) verlassen wird. Noch einmal zusammengefasst, und weil das für Ihr Denkmodell wichtig ist: Alle Objekte, deren Klasse mit dem Schlüsselwort class definiert wurde, sind auf dem managed heap angelegt. Der Programmierer hat den Zugriff auf das Objekt in Form einer Referenz (Zeiger). Da dies der Standard-Zugriff ist, ist auch keine explizite Syntax für Zeiger notwendig. Es wird daher immer und überall der Punktoperator verwendet. r1.WriteToConsole(); r2.WriteToConsole(); r3.WriteToConsole(); Hier wird also die Methode WriteToConsole() über die Objekte r1,r2 und r3 aufgerufen und die Werte auf dem Bildschirm ausgegeben.
47
C# 48
2 Ein kleines Experiment soll Ihr Verständnis noch verbessern. CD-Beispiel Fraction2
public static void Main() { FractionExamples.Fraction r1 = new FractionExamples.Fraction (); FractionExamples.Fraction r2; r1.WriteToConsole ();
//Ausgabe von r1
r2 = r1; //r1 und r2 zeigen nun auf dasselbe Objekt r2.WriteToConsole (); //dieselbe Ausgabe wie r1 r2.z = 1; //wir ändern das Objekt über r2 r1.WriteToConsole(); //Ausgabe des Objektes über r1 } Bei diesem Beispiel wurde eine Referenz r2 definiert, aber diese Referenz zeigt auf kein Objekt. Der Kompiler wird hier einen Fehler ausgeben, wenn Sie versuchen würden, auf r2 eine Aktion durchzuführen. Sie können aber r2 als Referenz eines bestehenden Objektes verwenden. Die Zeile r2 = r1; //r1 und r2 zeigen nun auf dasselbe Objekt
Zwei Referenzen auf dasselbe Objekt Abb. 2.9
C # – die neue Programmiersprache 2
49
führt dies durch. Dass hier r2 und r1 tatsächlich auf dasselbe Objekt verweisen, wird in diesem Beispiel gezeigt, indem das Objekt über r2 verändert wird, die Ausgabe aber über r1 erfolgt. Zur Verdeutlichung wird hier der Sachverhalt noch einmal grafisch dargestellt.
Grunddatentypen Im Beispiel wurde der Datentyp System.Int32 verwendet. Das ist ein Grunddatentyp, den die .NET-Laufzeitumgebung im Namensraum System anbietet. C# erlaubt aber auch, statt System.Int32 das jedem C/C++-Programmierer bekannte Schlüsselwort int zu verwenden. Das Schlüsselwort int ist aber nichts anderes als ein Synonym der Sprache C# für System.Int32. Weitere Grunddatentypen der .NET-Laufzeitumgebung und die entsprechenden C#-Synonyme sind in der folgenden Tabelle dargestellt. .NET-Datentyp
C#-Schlüsselwort
Beschreibung
Tab. 2.1:
System.Boolean
bool
True oder False
System.Char
char
Unicode (2Byte) Zeichen
C#-Schlüsselwörter für .NET-Grunddatentypen
System.Sbyte
sbyte
ganzzahliger Typ (1 Byte) mit Vorzeichen -128 bis +127
System.Int16
short
ganzzahliger Typ (2 Byte) mit Vorzeichen
System.Int32
int
ganzzahliger Typ (4 Byte) mit Vorzeichen
System.Int64
long
ganzzahliger Typ (8 Byte) mit Vorzeichen
Sytem.Byte
byte
ganzzahliger Typ (1 Byte) ohne V*orzeichen 0 – 255
System.UInt16
ushort
ganzzahliger Typ (2 Byte) ohne Vorzeichen
System.UInt32
uint
ganzzahliger Typ (4 Byte) ohne Vorzeichen
System.UInt64
ulong
ganzzahliger Typ (8 Byte) ohne Vorzeichen
System.Single
float
Fließkommazahl (32 Bit)
System.Double
double
Fließkommazahl(64Bit)
System.Decimal
decimal
Fließkommazahl (96Bit)
System.String
string
Stringtyp (Unicode)
C# 50
2 Verwenden Sie die C#-Schlüsselwörter statt den generischen .NET-Typen. Die Lesbarkeit des Codes wird dadurch deutlich verbessert. .NET fasst diese Grunddatentypen unter dem Namen CTS (common type system) zusammen. Alle .NET-Sprachen (C#, VB.NET, MC++ etc.) müssen diese gemeinsamen Typen unterstützen. Die Syntax der speziellen Sprachen für Grunddatentypen verweist immer auf einen der Typen im CTS. Dies ist eine wichtige Voraussetzung für die Interoperabilität zwischen .NET-Sprachen.
Schlüsselwort using Ebenfalls eine Verbesserung der Lesbarkeit ergibt die Verwendung des Schlüsselwortes using. Damit kann ein Namensraum „geöffnet“ werden, sodass für Klassen in diesem Namensraum die explizite Angabe des Namensraumes nicht mehr notwendig ist. Mit der Verwendung des Schlüsselwortes using sowie der C#-Schlüsselwörter für die Grunddatentypen schaut der Code nun so aus: CD-Beispiel Fraction3
using System; using FractionExamples; namespace FractionExamples { class Fraction { public int z; //Zähler public int n; //Nenner public Fraction () { z = 0; n = 1; } public Fraction (int z) { this.z = z; n = 1; } public Fraction (int z, int n)
C # – die neue Programmiersprache 2 { this.z = z; this. n = n; } public void WriteToConsole() { Console.WriteLine("{0}/{1}", z, n); } } } //Hauptprogramm für Testzwecke class App { public static void Main() { Fraction r1 = new Fraction (); Fraction r2; r1.WriteToConsole(); //Ausgabe von r1 r2 = r1; //r1 und r2 zeigen nun auf dasselbe Objekt r2.WriteToConsole();
//dieselbe Ausgabe wie r1
r2.z = 1; //wir ändern das Objekt über r2 r1.WriteToConsole();//Ausgabe des Objektes über r1 } }
System.Object Unter .NET müssen alle Klassen von System.Object abgeleitet sein. Unter C# ist dies implizit durch die Definition mit dem Schlüsselwort class geschehen. Damit erbt jeder Datentyp schon eine Menge von diesem Datentyp. Interessant ist jetzt natürlich zu wissen, welche Methoden und Eigenschaften die Klasse System.Object implementiert.
51
C# 52
2 Boolean Equals(Object)
Testet auf Gleichheit und gibt True zurück, wenn gleich
Int32 GetHashCode()
Die Methode GetHashCode gibt eine eindeutige, das Objekt bezeichnende ganzzahlige Zahl zurück. Wenn zwei Objekte desselben Typs gleich sind, dann haben diese auch denselben hash code. Kann vielfältig verwendet werden (z.B. Sortieralgorithmus etc.).
Type GetType()
Gibt ein Type-Objekt zurück. Damit kann dann direkt auf die Metadaten des Typs zurückgegriffen werden. Mehr dazu später!
String ToString()
Gibt den Namen der Klasse als String zurück. System.WriteLine verwendet übrigens die Methode ToString() eines Objektes zur Ausgabe!
Experimentieren Sie mit den Methoden der Basisklasse: Console.WriteLine(r1.GetHashCode()); Console.WriteLine(r1.ToString()); Console.WriteLine(r1.Equals(r2)); Console.WriteLine(r1.GetType()); Sie erhalten eine Ausgabe ähnlich der folgenden: 4 DotNetSeminar.Fraction True DotNetSeminar.Fraction Bis auf die Methode GetType() können alle in der abgeleiteten Klasse überschrieben werden. Dies geschieht durch das Schlüsselwort override. Dazu werden Sie später noch einiges mehr hören. Im Beispiel ist es sinnvoll, die Methode ToString() zu überschreiben, anstatt eine Methode WriteToConsole() zu implementieren. Dies kann dann wie folgt geschehen: CD-Beispiel Fraction4
override public String ToString() { string s;
C # – die neue Programmiersprache 2 s = String.Format("{0}/{1}",z,n); return s; } Die Klasse Fraction kann nun ganz „natürlich“ von der Console.WriteLine(...)-Methode verwendet werden, da WriteLine immer die ToString (...)-Methode auf die Objekte anwendet. Console.WriteLine(r1); Console.WriteLine( "Die rationale Zahl hat den Wert {0}",r1); Das Überschreiben von virtuellen Methoden folgt später noch im Detail.
Strukturen vs. Klassen Im Einführungsbeispiel haben Sie einen ersten Eindruck bekommen, wie Objekte im Speicher verwaltet werden. Wesentlicher Kernpunkt der CLR (Common Language Runtime) ist der „managed heap“ (zu deutsch „verwaltete Halde“). Sie haben auch festgestellt, dass sämtliche Objekte vom Typ class im „managed heap“ angelegt werden und dass auf dem Stack eine Referenzvariable entsteht. class FractionExamples { public int z; public int n; ... }
Referenzobjekt Abb. 2.10
53
C# 54
2 Dies gilt für alle Typen, die von System.Object abgeleitet sind. Unter C# geschieht dies implizit durch das Schlüsselwort class. Bezüglich der Auflösung des Objektes muss sich der Programmierer keine Gedanken machen, dies wird vom Garbage Collector durchgeführt (sobald im Stack keine Referenzvariablen existieren, darf der GC das Objekt im „managed heap“ auflösen). Der Namensraum System besitzt aber auch noch eine andere Klasse, nämlich System.ValueType. Eine Ableitung von dieser Klasse hat ein anderes Verhalten zur Folge. Damit wird kein Objekt erzeugt, das sich auf dem „managed heap“ befindet und eine Referenzvariable auf dem Stack hat, sondern das Objekt wird direkt im Stack angelegt. Unter C# werden solche Typen mit dem Schlüsselwortes struct definiert. struct Fraction { public int z; public int n; ... }
Wertobjekt Abb. 2.11
Wie aus der Grafik ersichtlich, wird das Objekt nicht im „managed heap“ angelegt, sondern direkt auf dem Stack. Das Objekt löst sich natürlich auf, wenn der Stack „stirbt“. Eine solches Objekt nennt sich „Wertobjekt“ (valued object) im Gegensatz zum „Referenzobjekt“ (referenced object). Unter C# müssen Sie
C # – die neue Programmiersprache 2 also bei der Typdeklaration entscheiden, ob Objekte eines Typs als Referenzobjekte oder aber als Wertobjekte auftreten werden. Das ist unter C/C++ gänzlich anders. Dort entscheiden die Speicherklasse und der Ort der Variablendefinition, ob eine Variable auf dem Stack oder aber auf dem Heap angelegt wird. Dynamisch allozierte Variablen oder Objekte werden unter C++ immer auf dem Heap angelegt. Beachten Sie diesen Unterschied! Außerdem sollten Sie erkennen, dass das Schlüsselwort struct unter C# eine vollkommen andere Bedeutung hat, als bei C und C++. Es stellt sich nun naturgemäß die Frage, wann soll ein Datentyp mittels class definiert werden bzw. wann mittels struct? Die Klasse System.ValueType überschreibt die virtuellen Methoden von System.Object in einer Form, die dem Verhalten von Wertvariablen entsprechen. Dies gilt im Besonderen auch für die Zuweisung. Führen Sie hierzu einige Experimente durch. using System; using FractionExamples; class Fraction { public int z; public int n; .. . } public static void Main() { Fraction r1 = new Fraction(3); Fraction r2; Console.WriteLine(r1); r2 = r1; r2.z = 5; //wir ändern das Objekt über r2 Console.WriteLine(r1); //wir geben das Objekt über r1 auf den //Bildschirm }
55
C# 56
2 Sie kennen das Verhalten des ursprünglichen Beispiels. Sie definieren nun aber die Klasse Fraction mit dem Schlüsselwort struct. Der Kompiler meckert noch mit einer Fehlermeldung, dass bei Werttypen kein expliziter Default-Konstruktor implementiert werden kann. Sie entfernen diesen Code und starten einen neuen Kompiliervorgang. CD-Beispiel Fraction5
struct Fraction { public int z; public int n; /*public Fraction () { z = 0; n = 1; }*/ ... } public static void Main() { Fraction r1 = new Fraction (3,2); Fraction r2; Console.WriteLine(r1); r2 = r1; //r2 wird mit den Werten von r1 belegt r2.z = 5; //wir ändern r2 Console.WriteLine(r1); //keine Änderung von r1 feststellbar } Sie sehen, es handelt sich bei r1 und r2 tatsächlich um voneinander unabhängige Objekte! Dieses Verhalten wird gerne bei „primitiven“ Datentypen gesehen. Bei einer Zuweisung wird der Wert kopiert, und nicht die Referenz. Beim Datentyp Fraction wird man eher dieses Verhalten erwarten, und aus diesem Gesichtspunkt ist es sicherlich überlegenswert, Fraction mit struct denn mit class zu definieren. Übrigens sind die meisten Grunddatentypen der .NET-Laufzeitumgebung Werttypen.
C # – die neue Programmiersprache 2 Boxing und Unboxing In vielen Situationen kommt es vor, dass ein Werteobjekt das Verhalten eines Referenzobjektes haben sollte. Es ist möglich, ein Werteobjekt in eine Referenzinstanz zu konvertieren. Dieser Vorgang wird „boxing“ genannt (der umgekehrte Vorgang wird „unboxing“ genannt). Ein kleines Beispiel zum Experimentieren. Fraction sei mittels struct definiert worden und daher ein Werttyp. public static void Main() { Fraction r1 = new Fraction(1); //Wertobjekt (Stack) Fraction r2; //Wertobjekt (Stack) System.Object o1; //Referenz (Objekt //existiert noch nicht) object o2; //detto o1 = r1; o2 = o1;
//boxing findet statt (Heap) //Referenzzuweisung //(auf dasselbe Objekt)
r2 = (Fraction)o2; //unboxing findet statt r1.z = 2; r2.z = 3;
//Aenderung von r1 //Aenderung von r2
Console.WriteLine(r1); Console.WriteLine(o1); Console.WriteLine(o2); Console.WriteLine(r2); } r1 wird auf dem Stack angelegt und mit 1/1 vorbelegt. Außerdem wird auf dem Stack Speicherplatz für r2 angelegt. Auf dem Stack werden dann zwei Referenzen (Zeigervariablen) vom Typ System.Object (object) angelegt. Bei der Zeile o1 = r1 findet nun dieses boxing statt. Syntaktisch weisen Sie hier einer Referenz einen Wert zu. Was passiert nun? Auf dem managed heap wird Speicherplatz angelegt und dieser wird mit den Daten des Speicherplatzes auf dem Stack (r1) belegt. o2 = o1 ist Ihnen aus dem Einführungsbeispiel bekannt, o2 zeigt auf dasselbe Objekt wie o1.
CD-Beispiel Fraction6
57
C# 58
2 Bei r2 = (fraction)o2 findet ein unboxing statt. Der Speicherplatz auf dem Stack (r2) wird nun mit den Daten des Objektes, das sich auf dem Heap befindet belegt. Die Ausgabe, die das Beispiel auf der Konsole erzeugt, demonstriert das Verhalten deutlich. Interessant ist auch die Zeile Console.WriteLine(o1). Es wird nämlich für die Ausgabe die ToString(...)-Methode von Fraction verwendet. Ein deutlicher Hinweis, dass die Methode ToString() der Klasse System.Object eine virtuelle Methode darstellt. Mehr dazu aber später. Boxing kann implizit und explizit geschehen. Explizites boxing ist syntaktisch dem Casting von C/C++ ähnlich, aber natürlich viel typsicherer. Implizites (automatisches) boxing hat auch einen ganz besonderen syntaktischen Effekt, der nur mit dem boxing-Konzept erklärbar ist. 3.ToString(); 3 ist ganz klar ein Wertobjekt und muss daher ein boxing erfahren, wird somit zu einem Objekt erhoben, und dann wird die Methode ToString() ausgeführt wird. Warum Console.WriteLine(3); funktioniert, sollte nun auch klar sein. In den weiteren Beispielen wird der Datentyp Fraction wieder mit dem Schlüsselwort class definiert.
Enumerationen Aufzählungen (Enumerationen) kennen Sie sicherlich aus den Programmiersprachen C und C++. Es handelt sich dabei um ganzzahlige Typen, die vom Benutzer definiert werden können. Enumerationsinstanzen können dann nur Werte zugeordnet werden, die in der Definition der Enumeration festgelegt wurde. Darüber hinaus können diesen ganzzahligen Werten auch noch anwendungsfreundliche Namen zugeordnet werden. Unter C/C++ konnten Enumerationsinstanzen auch direkt die ganzzahligen Werte zugeordnet werden. Der Kompiler hat dies akzeptiert, was oft zu Fehlern führte, weil unabsichtlich
C # – die neue Programmiersprache 2 Werte zugeordnet werden konnten, die in der Enumerationsdefinition gar nicht vorgekommen sind. Unter C# ist dies nicht mehr möglich. Hier ein kleiner Codeausschnitt, der Ihnen die Funktionsweise von Enumerationen unter C# verdeutlichen soll. using System; public enum Week { Monday=0, Tuesday=1, Wednesday=2, Thursday=3, Friday=4, Saturday=5, Sunday=6 } class App { public static void Main() { Week Day = Week.Monday; //int iDay = Week.Monday; nicht erlaubt!!!! //Day = 5; ebenfalls nicht erlaubt!!!!! switch(Day) { case Week.Monday: //mach etwas break; case Week.Thursday: //mach etwas break; } } }
CD-Beispiel Enumeration1
59
C# 60
2
Methoden Methode mit Bindung Fügen Sie dem Beispiel eine Methode zur Addition von Bruchzahlen hinzu. (Fraction soll wieder als Referenztyp definiert werden). Aus der Grundschule kennen Sie sicherlich noch den Algorithmus, der in der Methode Add implementiert ist. CD-Beispiel Fraction7
public Fraction Add(Fraction r) { Fraction erg = new Fraction(); erg.z = n*r.z + z*r.n; erg.n = n * r.n; return erg; } Im Gegensatz zu C++ ist die Angabe der Schutzklasse explizit bei jeder Methode notwendig (public). Rückgabewert der Methode ist vom Typ Fraction (Ergebnis der Addition). In der Implementierung wird eine Ergebnisvariable erzeugt, die das Additionsergebnis aufnimmt und zurückgibt. Der Aufruf der Methode kann dann wie folgt geschehen: class App { public static void Main() { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction (5,2); Fraction r3; r3 = r1.Add(r2); Console.WriteLine(r3); } } Als Ergebnis der Addition sollte dann 16/4 am Bildschirm erscheinen (kürzen kann die Applikation leider noch nicht).
C # – die neue Programmiersprache 2 Statische Methode Die Additionsmethode könnten Sie auch statisch implementieren. public static Fraction Add(Fraction r1,Fraction r2) { Fraction erg = new Fraction(); erg.z = r1.z*r2.n + r1.n*r2.z;; erg.n = r1.z * r2.n; return erg; } Sie wissen, eine statische Methode hat keine Bindung zu einem Objekt. Der Aufruf im Hauptprogramm: public static void Main() { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction (5,2); Fraction r3; Fraction r4; r3 = r1.Add(r2); r4 = Fraction.Add(r1,r2); Console.WriteLine(r3); Console.WriteLine(r4); } Sie sehen, bei der statischen Methode ist natürlich die Angabe des Namensraumes notwendig. Es ist eine Geschmackssache, welche Variante lesbarer ist, in der numerischen Algebra mag die statische Variante syntaktisch vielleicht einen Vorteil haben. Aber wie Sie sehen, können auch beide Varianten gleichzeitig implementiert werden.
Überladen von Methoden Wie unter C++ können auch unter C# Methoden überladen werden, d.h. es können gleiche Methodennamen verwendet werden, diese müssen sich aber in der Anzahl bzw. Typen der Parameter unterscheiden. Genau dss ist im Beispiel bei der Implementierung der Additionsmethode passiert.
CD-Beispiel Fraction7
61
C# 62
2 Überladen von Operatoren Ähnlich C++ können auch unter C# hinter Operatoren Methoden definiert werden, die bei Verwendung des Operators im richtigen Kontext aufgerufen werden. Vor allem bei mathematischen Typen ist das oft sinnvoll. So wird im nächsten Beispiel der +-Operator mit der Additionsfunktionalität überlagert. CD Bespiel Fraction8
public static Fraction operator+( Fraction r1, Fraction r2) { Fraction erg = new Fraction (); erg.z = r1.z*r2.n + r1.n*r2.z; erg.n = r1.n * r2.n; return erg; } public static void Main() { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction (5,2); Fraction r3; Fraction r4; Fraction r5; //Aufruf über statische Methode r3 = Fraction.Add(r1,r2); //Aufruf über Methode mit Bindung r4 = r1.Add(r2); //überladener Operator + r5 = r1+r2; Console.WriteLine(r3); Console.WriteLine(r4); Console.WriteLine(r5); Console.WriteLine(r1+r2); } Aber auch Operatoren können mehrfach überladen werden. Im folgenden Beispiel wird der +-Operator so überladen, dass ein Fracition-Typ und eine Int32-Typ miteinander addiert werden können. public static Fraction operator+( Fraction r1, int i2) {
C # – die neue Programmiersprache 2 Fraction r2 = new Fraction (i2); return r1+r2; } Somit ist auch folgende Anweisung möglich: r6 = r1 + 5; Beachten Sie als C++-Programmierer aber, dass überlagerte Operatormethoden immer(!) static und public vereinbart sein müssen.
Vergleich von Objekten Es ist auch möglich, die Operatoren == und != zu überladen. Hierzu aber erst einige Gedanken. Die Default-Implementierung von == hat folgendes Verhalten: Bei Referenzobjekten wird true zurückgegeben, wenn mit den Referenzen dasselbe Objekt verwiesen wird. Bei Wertobjekten wird true zurückgegeben, wenn die Werte übereinstimmen. Sie sehen daraus, dass ein Vergleich der Speicherbereiche auf dem Stack stattfindet. Dieses Verhalten können Sie natürlich überlagern. Im FractionBeispiel ist es sicherlich sinnvoll, bei Gleichheit des Inhaltes true zurückzugeben. Hierzu aber noch mehr unter bei der Basisklasse System.Object.
Nicht überladbare Operatoren Beachten Sie, dass einige Operatoren, die unter C++ überladbar sind, unter C# nicht überladen werden können. Das sind: &&, || , (), [ ], = , -> , new Auffallend ist, dass auch die Zuweisung nicht überladbar ist, was aber nach genaueren Überlegungen auch nicht notwendig ist (in C++ aber sehr wohl). Der [ ]-Operator (Indexoperator) kann ebenfalls nicht überladen werden, aber es gibt hier unter C# das Konzept des „indexers“, das Sie in einem späteren Kapitel kennen lernen werden. Überlagern Sie nun die weiteren Operatoren, die binären Operatoren (diese brauchen zwei Operanden) +,-,*,/ sowie die
63
C# 64
2 unären Operatoren – und ~, hinter denen Sie den Kehrwert implementieren. Auf der Begleit-CD dieses Buches finden Sie die Lösungen im Projekt Fraction8.
ref- und out- Parameter Angenommen, Sie wollen in der Klasse Fraction eine Methode implementieren, die mit einem Aufruf die Werte der MemberVariablen z und n zurückgeben sollen. Dass eine Implementierung in der Form public void GetZN(int z, int n) { z = this.z; n = this.n; } mit folgendem Aufruf nicht zielführend ist, wird einem erfahrenen C/C++-Programmierer klar sein. public static void Main() { Fraction r1 = new Fraction(3,2); int z = 0; int n = 0; Console.WriteLine(z); Console.WriteLine(n); r1.GetND(z,z); Console.WriteLine(z); Console.WriteLine(n); } Da die Typen System.Int32 und damit int Wertobjekte darstellen (weil mittels struct definiert), ist dieses Verhalten erklärbar. Werte werden bei Funktionsübergabe kopiert. In der Implementierung werden also die Kopien manipuliert, was aber dann zur Folge hat, dass die eigentlich übergebenen Parameter keine Änderung erfahren haben. In C/C++ kennen Sie die Lösung, Sie übergeben Adressen bzw. deklarieren den/die Parameter als C++-Referenzen. Unter C# gibt es einen ähnli-
C # – die neue Programmiersprache 2 chen Mechanismus. Obiges Beispiel müssten Sie wie folgt richtig implementieren: public void GetZN(ref int z,ref int n) { z = this.z; n = this.n; }
CD-Beispiel Fraction9
public static void Main() { Fraction r1 = new Fraction(3,2); int z = 0; int n = 0; Console.WriteLine(z); Console.WriteLine(n); r1.GetZN(ref z, ref n); Console.WriteLine(z); Console.WriteLine(n); } Mit dem Schlüsselwort ref geben Sie an, dass hier technisch eine Referenz und nicht der Wert übergeben werden sollte. Auffallend ist aber, dass auch beim Aufruf das Schlüsselwort ref explizit angegeben werden muss! Was ist aber der Fall, wenn der Parameter, der in der Methode eine Änderung erfahren sollte, ein Referenztyp ist? Dann ist das Schlüsselwort ref nicht notwendig, weder bei der Implementierung noch beim Aufruf. Nachfolgend wird Ihnen das an einem zugegebenermaßen akademischen Beispiel demonstriert. class App { public static void DoubleFraction(Fraction r) { r.n = r.n * 2; } public static void Main() {
CD-Beispiel Fraction9
65
C# 66
2 Fraction r1 = new Fraction(3,2); DoubleFraction(r1); Console.WriteLine(r1); ... } } Im Namensraum App wird eine statische Funktion DoubleFraction implementiert, die als Übergabeparameter einen Typ Fraction (Referenztyp) hat und verdoppelt intern die MemberVariable z. Verifizieren Sie im Beispiel, dass r1 tatsächlich verdoppelt! Vielleicht ist Ihnen aufgefallen, dass im obigen Beispiel die Variablen z und n mit 0 vorbelegt wurden. int z = 0; int n = 0; Geschieht dies nicht (keine Initialisierung), dann meckert der Kompiler mit einer entsprechenden Fehlermeldung. public static void Main() { Fraction r1 = new Fraction(3,2); int z; //nicht explizit vorbelegt int n; r1.GetZN(ref z, ref n); Console.WriteLine(z); Console.WriteLine(n); } Bei Wertetypen unangenehm, denn was ist, wenn Sie eine Methode implementieren wollen, dessen Aufgabe es eben ist, ein Objekt zu initialisieren. Dafür wurde das Schlüsselwort out eingeführt. public { z = n = } public
void GetZN(out int z, out int n) this.z; this.n; static void Main()
C # – die neue Programmiersprache 2 { Fraction r1 = new Fraction(3,2); int z; int n; r1.GetZN(out z,out n); Console.WriteLine(z); Console.WriteLine(n); } Wenn die Parameter mit dem Schlüsselwort out gekennzeichnet sind, dann akzeptiert der Kompiler dies, auch wenn die Variablen z und n nicht initialisiert wurden. Der Kompiler erkennt, dass dies beim Methodenaufruf GetZN(...) geschieht. Dass hier implizit eine Referenzübergabe erfolgt, ist selbstverständlich.
Eigenschaften (Properties) Im deutschsprachigen Raum versteht man unter den Eigenschaften von Klassen im Wesentlichen die Member-Variablen. Die Klasse Fraction besitzt zwei Eigenschaften vom Typ int, die auch public vereinbart sind, und somit jederzeit direkt von „außen“ veränderbar sind. Das kann aber in vielen Fällen nicht gut sein, da damit ein Programmierer auch Werte zuordnen könnte, die logisch nicht erlaubt sind. So kann einem Programmierer nicht verboten werden, den Nenner eines Objektes vom Typ Fraction in seinem Programm explizit auf 0 zu stellen, was natürlich mathematisch Unfug ist. Die Lösung hierzu besteht darin, dass diese Eigenschaften private vereinbart werden, und damit ein direkter Zugriff nicht mehr möglich ist. class Fraction { private int z; private int n; ... } Nun wird der Kompiler mit einer Fehlermeldung aufwarten, sobald Sie versuchen werden, z oder n eines Objektes zu ändern. Sollten aber Änderungen direkter Members möglich sein,
67
C# 68
2 dann müssen hier eben entsprechend Methoden angeboten werden, die z.B. so aussehen könnten: public int GetZ() { return z; } public int GetN() { return n; } public void SetZ(int z) { this.z = z; } public void SetN(int n) { //0 für den Nenner ist verboten if(n == 0) n=1; this.den = n; } Damit kann nun innerhalb der Methoden reagiert werden, wenn es zu einer Zuweisung von ungültigen Werten kommt. (Zugegeben, die Implementierung von SetN(...) ist nicht zufriedenstellend. Die Verwendung des Exception-Mechanismus würde sich hier anbieten. Dazu aber später mehr.) Nachteilig an dieser Variante ist, dass die Lesbarkeit der Programme doch deutlich abnimmt. C# bietet aber hier einen neuen Mechanismus an, der nachfolgend vorgestellt wird: CD-Beispiel Fraction10
private int z; private int n; public int N { get { return n; } set {
C # – die neue Programmiersprache 2 n=value; if(n==0) n=1; } } Drei neue Schlüsselwörter lernen Sie hier kennen. In den Blöcken, die mit set bzw. get eingeleitet werden, wird die Funktionalität programmiert, die durchgeführt werden soll, wenn syntaktisch auf N zugegriffen wird. Im lesenden Zugriff (get) wird hier unmittelbar der Wert der privaten Member-Variable n zurückgegeben. Bei einem schreibenden Zugriff auf N (set) kann über das Schlüsselwort value auf den Wert zugegriffen werden, der bei einer Zuweisung auf die Member-Variable N verwendet wird. Fraction r = new Fraction(3,2); r.N = 0; //Schreibender Zugriff int n = r.N; //Lesender Zugriff Was nun ausschaut wie der Zugriff auf eine Member-Variable ist eigentlich ein Methodenaufruf. Wenn Sie auf den set-Block verzichten, dann kann nur lesend auf N zugegriffen werden, wenn Sie nur den set-Block implementieren, dann hätten Sie nur schreibenden Zugriff auf N. Die Verwendung von Properties sind in der gezeigten Form sehr zu empfehlen, da diese die Lesbarkeit des Codes doch deutlich steigern. Im Deutschen wird der Ausdruck „Eigenschaft“ gerne im Zusammenhang mit Member-Variablen verwendet. Deshalb wird hier der Terminus „Eigenschaft“ vermieden, und das (deutschenglische) Kunstwort Member-Variable verwendet. Im Folgenden wird auch der Ausdruck Properties gebraucht.
Ausnahmeverarbeitung – Exception Handling (EH) Vielleicht kennen Sie den Exception-Handling (EH) Mechanismus von C++ oder auch SEH (Structured Exception Handling) des Betriebssystems Windows. Unter C++ kann das EH optional verwendet werden, da Exception-Handling unter C++ eine „teure“ Sache in Bezug auf Laufzeit und Speicherbedarf dar-
69
C# 70
2 stellt. .NET und damit C# bietet ebenfalls einen Exception-Mechanismus an, der nachfolgend vorgestellt wird. In vielen Frameworks und APIs (Application Programming Interface) ist und war es sehr populär, Funktionen mit einem Rückgabewert zu versehen, der in irgendeiner Form den Erfolg des Aufrufes dokumentiert. Dies kann ein Boolscher Wert sein oder aber in Form eines „Resultatwertes“, der je nach Wert einen Fehler oder Status repräsentiert. C++-Programmierer, die auf dem COM-Programmiermodell entwickeln, kennen den Datentyp HRESULT (32 bit), der diese Funktion erfüllt. Prinzipiell ist dieser Ansatz auch unter C# möglich, aber nicht zu empfehlen. bool f = Methode(); if(f== false) { //Fehlerbehandlung durchführen } Es ist damit nämlich unter C/C++ und auch C# folgender Aufruf möglich. Methode(); Da der Programmierer in diesem Beispiel der Rückgabewert ignoriert, wird faktisch auf die Fehlerbehandlung gänzlich verzichtet. Und muss man sich jetzt wundern, dass Bugs in Programmen auftauchen? In der .NET-Laufzeitumgebung ist EH ein fundamentaler Bestandteil und es wird daher empfohlen, EH zu verwenden, statt Rückgabecodes, die still und heimlich auch ignoriert werden können. Wer den EH-Mechanismus von C++ kennt, wird alles sehr vertraut finden, trotzdem gibt es einige Unterschiede. Im Fraction-Beispiel werden Sie einen EH-Mechanismus bei der Division einführen. Sie wissen, auch bei Bruchzahlen ist eine Division durch 0 nicht erlaubt. Ist die z-Komponente (Zähler) des Divisors 0, dann müssen Sie mit einer entsprechenden Fehlerbehandlung reagieren, da sonst ein nicht konsistentes Fraction-Objekt entsteht, das einen Nenner von 0 hat!
C # – die neue Programmiersprache 2 try – catch – throw public static Fraction operator/( Fraction r1, Fraction r2) { if(r2.z == 0) throw new Exception("Achtung Division durch 0"); return r1 * ~r2; //Multiplikation mit Kehrwert } Hier prüft die Methode, ob der Zähler von r2 gleich 0 ist. Denn dann würde die Bruchzahl den Wert 0 darstellen. Ist dies der Fall, dann „werfen“ (throw) Sie eine Exception. „Werfen“ müssen Sie ein Objekt vom Typ System.Exception oder aber davon abgeleitet. Dieses Objekt erzeugen Sie dynamisch (hierzu existiert eine größere Anzahl von Konstruktoren). public static void Main() { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction (); // r2 hält 0/1 Fraction r3; r3 = r1/r2; //hier tritt eine Exception auf Console.WriteLine(r3); } Wenn nun das Programm in dieser Form durchgeführt wird, dann tritt eine Exception auf, d.h. das Programm beendet sich mit einem entsprechenden Hinweis. Nun das ist nicht gerade das „Gelbe vom Ei“, wenn sich das Programm, zwar mit einem Hinweis, aber endgültig verabschiedet. Sie wollen ja den Fehler behandeln bzw. auf einen Fehler entsprechend reagieren. public static void Main() { try { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction(); // r2 hält 0/1 Fraction r3;
CD-Beispiel Exception1
71
C# 72
2 r3 = r1/r2; //hier tritt eine Exception auf Console.WriteLine(r3); } catch(Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("Hier ist das Programm fertig"); } Sie schützen hier den Bereich im Hauptprogramm durch einen so genannten try-Block. Dieser Block wird auch guarded-Block genannt. Im Anschluss an einen try-Block folgen ein oder aber auch mehrere catch-Blöcke („fangen“), die eine solche Exception auffangen. D.h., wenn im guarded-Block eine Exception auftritt, dann wird unverzüglich der catch-Block angesprungen und die entsprechenden Anweisungen durchgeführt. Nach Ausführung dieser Anweisungen wird der Code hinter den catch-Blöcken weitergeführt. Es können auch mehrere catch-Blöcke definiert werden, um auf unterschiedliche Arten von Exceptions auch unterschiedlich zu reagieren. Das nachfolgende Beispiel soll das verdeutlichen. CD-Beispiel Exception2
class FractionException:Exception { string _message; int _ID; public string message { get{return _message;} } public int ID { get{return _ID;} } public FractionException(string message,int ID) { this._ID = ID; this._message = message; }
C # – die neue Programmiersprache 2 } class Fraction { . . . } Hier wird eine neue Klasse FractionException definiert. Diese Klasse ist von Exception abgleitet (genaueres über Vererbung folgt im Kapitel 3, Baugruppen (Assembly)), und kann daher auch im .NET-Exception-Mechanismus verwendet werden. Die Klasse implementiert zwei readonly-Properties (ID und message), die im Konstruktor belegt werden können. public static void Main() { try { Fraction r1 = new Fraction (3,2); Fraction r2 = new Fraction(); // r2 hält 0/1 Fraction r3; r3 = r1/r2; //hier tritt eine Exception auf Console.WriteLine(r3); } catch(FractionException e) { Console.WriteLine("Custom EH: {0} ID: {1}", e.message,e.ID); } catch(Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("Hier ist das Programm fertig"); } Sollte im guarded-Block eine Exeption auftreten, so ist diese vom Typ abhängig. Da die Methode Main() einen catch-Block für eine Exception vom Typ FractionException implementiert, „fällt“ das Programm bei Auftreten einer Fraction-Division durch 0 in diesen Block. Sämtliche anderen Exception werden vom allgemeinen catch-Block abgehandelt.
73
C# 74
2 Exceptions können auch verschachtelt sein. Ein kleines Experiment soll auch dies wieder verdeutlichen: CD-Beispiel Exception3
class App { public static void ExcTest(Fraction r2) { Console.WriteLine("ExcTest wird aufgerufen"); try { Fraction r1 = new Fraction(3,2); Fraction r3 = r1/r2; } catch(FractionException e) { Console.WriteLine("Custom EH: {0} ID: {1}", e.message,e.ID); } Console.WriteLine("ExcTest ist beendet"); } public static void Main() { try { ExcTest(new Fraction()); } catch(Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("Hier ist das Programm fertig"); } } Die Klasse App implementiert hier die statische Methode ExcTest, die eine Division durchführt. Dieser Codeteil ist in der Methode selbst in einem guarded-Block geschützt. Die Methode wird in Main(), ebenfalls in einem guarded-Block, ausgeführt. Tritt nun eine Exception in der Methode ExcTest auf, so wird natürlich der catch-Block in der Methode selbst verwendet. Die Ausgabe auf der Konsole wird Folgende sein:
C # – die neue Programmiersprache 2 ExcTest wird aufgerufen Custom EH: Achtung Division durch 0 ID: 5 ExcTest ist beendet Hier ist das Programm fertig Sie können allerdings in diesem catch-Block die Behandlung der Exception an den übergeordneten catch-Block weiterleiten. Mit dem Schlüsselwort throw, ohne Angabe eines ExceptionObjektes, wird dann direkt auf den übergeordneten catchBlock gesprungen. catch(FractionException e) { Console.WriteLine("Custom EH: {0} ID: {1}", e.message,e.ID); throw; //Weiterreichen } Die Ausgabe auf der Konsole bestätigt dies. ExcTest wird aufgerufen Custom EH: Achtung Division durch 0 ID: 5 Exception of type FractionExamples.FractionException was thrown. Hier ist das Programm fertig Beachten Sie, dass hier der Codeabschnitt in der Methode ExcTest nach dem catch-Block nicht durchgeführt wurde, was Sie aber nicht überraschen sollte, da die Exception ja sofort weitergereicht wurde.
finally Eine immer wieder auftretende Schwierigkeit gibt es aber beim EH. Wie Sie festgestellt haben, führt das Programm seine Tätigkeit nach den catch-Blöcken weiter. Dies kann insofern zu Schwierigkeiten führen, wenn bestimmte Ressourcen geöffnet sind (z.B. eine Datenbanksitzung), und dann wegen einer Exception nicht mehr freigegeben werden können. Für solche Fälle bietet C# eine Lösung mit dem Schlüsselwort finally. Nachfolgendes Beispiel soll die Verwendung verdeutlichen:
CD-Beispiel Exception4
75
C# 76
2 CD-Beispiel Exception4
public static void ExcTest(Fraction r2) { Console.WriteLine("ExcTest wird aufgerufen"); try { Fraction r1 = new Fraction(3,2); Fraction r3 = r1/r2; } catch(FractionException e) { Console.WriteLine("Custom EH: {0} ID: {1}", e.message,e.ID); throw; } finally { Console.WriteLine( "Diesen Codeabschnitt unbedingt durchfühen"); } Console.WriteLine("ExcTest ist beendet"); } Zu jedem guarded-Block kann ein finally-Block zugeordnet werden. Dieser wird aufgerufen, egal was im guarded-Block passiert. Die Ausgabe auf der Konsole bestätigt dies: ExcTest wird aufgerufen Custom EH: Achtung Division durch 0 ID: 5 Diesen Codeabschnitt unbedingt durchführen Exception of type FractionExamples.FractionException was thrown. Hier ist das Programm fertig
Performanz Warum ist EH unter C++ aufwändig und hat großen Einfluss auf Codelänge und Laufzeit? EH unter C++ ist deshalb sehr aufwändig, da sämtliche Objekte, die im guarded-Block angelegt wurden, entsprechend sicher auch aufgelöst werden müssen, indem der Destruktor aufgerufen wird. Dies auch im Falle einer Exception. Wäre dies nicht der Fall, dann könnten böse Speicherlecks auftreten.
C # – die neue Programmiersprache 2 Ebenfalls müssen Objekte, die auf dem Stack angelegt sind, mit entsprechenden Destruktoraufrufen aufgelöst werden. Stack-Unwinding wird das genannt und ist sehr aufwändig. Die Aufgabe des Kompilers ist es, hier entsprechende Vorsorge zu tragen. C++-Programme, die EH verwenden, sind meist spürbar langsamer. Das ist ein wesentlicher Grund, warum viele Programmierer auf C++ EH verzichten. Nun fragen Sie sich sicherlich, wie schaut dies unter C# aus. Nun hier ist für die Auflösung der Objekte nicht der Kompiler zuständig, sondern der Garbage Collector (GC). Daher ist EH unter .NET um einiges schneller und der Overhead zur Laufzeit ist vernachlässigbar. Verwenden Sie deshalb unter C# EH fleißig, es wird zu deutlich stabileren Programmen führen, ohne dass diese auch an Performanz verlieren.
Vererbung Das Konzept der Vererbung (Wiederverwenden von Code über Vererbung) ist eines der wichtigsten in der objektorientierten Softwareentwicklung überhaupt. Sie werden diese Konzepte, die C# natürlich unterstützt, anhand eines Beispiels kennen lernen, das im Buch „Inside Visual C++“ von David Kruglinski verwendet wurde. Dieses Beispiel eignet sich gut, um das Konzept der Vererbung zu verdeutlichen. In einem Weltraumsimulationsprogramm bewegen sich die unterschiedlichsten Massen im Raum (Sonnen, Planten, Monde, Raumschiffe etc.). Alle Massen gehorchen den physikalischen Gesetzen, unabhängig ihrer Erscheinungsform. Die gemeinsamen Eigenschaften könnten in einer eigenen Klasse definiert und implementiert werden, sämtliche Erscheinungsformen werden dann diese Klasse als Basisklasse verwenden. using System; class Orbiter { protected decimal x; protected decimal y; protected decimal z; protected string name;
CD-Beispiel Orbiter1
77
C# 78
2 public Orbiter(string name, decimal x, decimal y, decimal z) { this.x = x; this.y = y; this.z = z; this.name = name; } public void Display() { Console.WriteLine("Orbiterobjekt {0}|{1}|{2}|{3})", name,x,y,z); } } class App { public static void Main() { Orbiter o = new Orbiter("o",100.0M,0.0M,0.0M); o.Display(); } } Erzeugen Sie ein neues Projekt, nennen Sie es Space und implementieren Sie die Klasse Orbiter. Natürlich werden Sie kein ausgewachsenes Simulationsprogramm erstellen, sondern beschränken die Funktionalität der Klasse auf den Namen eines Objektes sowie auf die Position des Objektes. Diese Eigenschaften (name,x,y,z) sollen Member-Variablen der Klasse Orbiter darstellen. In einem Simulationsprogramm sollten diese Körper natürlich dargestellt werden. Bezüglich der Darstellung wird die Klasse auf eine Ausgabe auf die Konsole beschränkt (DisplayMethode), die den Namen und die Position des Objektes ausgibt. (Zugegeben, Sie brauchen schon ein gutes Marketing, wenn Sie mit diesem Programm Geld verdienen wollten.) In einem ersten Versuch wird die Klasse gestestet. Beachten Sie, dass für die Darstellung der Koordinaten der Typ decimal verwendet wird (Fließkommatyp mit 96 Bits). Konstante Werte vom Typ decimal verlangen den Buchstaben M (groß M ) im Anschluss an den Zahlenwert.
C # – die neue Programmiersprache 2 Erweitern Sie das Simulationsprogramm, indem Sie zwei neue Klassen einführen, eine für Planeten und eine für Raumschiffe. class Planet : Orbiter { private decimal radius; public Planet(string name, decimal x, decimal y, decimal z, decimal radius):base(name,x,y,z) { this.radius = radius; } new public void Display() { Console.WriteLine( "Planetobject {0} ({1}|{2}|{3}|r={4}km)", name,x,y,z,radius); } } class Spaceship : Orbiter { private decimal fuel; public Spaceship(string name, decimal x, decimal y, decimal z, decimal fuel):base(name,x,y,z) { this.fuel=fuel; } new public void Display() { Console.WriteLine( "Spacshipobject {0} ({1}|{2}|{3}|f={4}kg)", name,x,y,z,fuel); } }
CD-Beispiel Orbiter2
79
C# 80
2 Das Ableiten funktioniert ähnlich wie in C++, indem Sie nach dem Namen der neuen Klasse einen Doppelpunkt gefolgt vom Namen der Klasse angeben, von der Sie die Eigenschaften und Funktionalität erben wollen. class Planet : Orbiter Die zusätzlichen Eigenschaften von Planet werden nun in bekannter Weise als Member-Variablen hinzugefügt. Die Klasse Planet erhält eine zusätzliche Member-Variable radius und einen eigenen Konstruktor für die Belegung der Members. Es ist naheliegend, im Planet-Konstruktor für die Belegung der Members, die von Orbiter geerbt wurde, den Konstruktor der Basisklasse zu verwenden. Das geschieht mit dieser Zeilen. public Planet(string name, decimal x, decimal y, decimal z, decimal radius):base(name,x,y,z) Ähnlich wie bei C++ wird der Basisklassenkonstruktor angegeben. Im Unterschied zu C++, wo der Name der Basisklasse angegeben wird, verwendet C# das Schlüsselwort base. Die Implementierung der Methode Display() der Klasse Planet als auch der Klasse Spaceship werden nun aber entsprechend angepasst: new public void Display() { Console.WriteLine( "Planetobject {0} ({1}|{2}|{3}|r={4}km)", name,x,y,z,radius); } bzw. new public void Display() { Console.WriteLine( "Spacshipobject {0} ({1}|{2}|{3}|f={4}kg)", name,x,y,z,fuel); }
C # – die neue Programmiersprache 2 Da auch schon die Basisklasse eine Methode Display() besitzt, müssen Sie das Schlüsselwort new angeben, um dem Kompiler explizit zu zeigen, dass Sie die Methode überschreiben wollen. Weiterhin erwähnenswert ist die Verwendung der Schutzklasse protected für die Members name,x,y,z in der Orbiter-Klasse. Wenn dies nicht gemacht worden wäre, sondern Sie als Schutzklasse private angegeben hätten, würde sich der Kompiler bei der Methode Display der Klasse Planet mit einem Fehler melden, da auf die Members der Basisklasse direkt zugegriffen wird. Genauere Erläuterungen zum Schutzkonzept folgen gleich. public static void Main() { Orbiter o = new Orbiter("o",100.0M,0.0M,0.0M); Planet p = new Planet("Jupiter", 1000.0M,200.3M,235.45M,353.2M); Spaceship s = new Spaceship("Enterprise", 20.3M,31.5M,83.8M,1000M); o.Display(); p.Display(); s.Display(); } Das Hauptprogramm zeigt die Verwendung der neuen Typen und auf der Konsole sollte sich folgende Ausgabe zeigen: Orbiterobjekt (o|100|0|0) Planetobject Jupiter (1000|200,3|235,45|r=353,2km) Spacshipobject Enterprise (20,3|31,5|83,8|f=1000kg) Sie sehen, die Display-Methode in den abgeleiteten Klassen wird überschrieben und kommt nicht zu Tage. Wollte man diese aufrufen (vorhanden ist sie ja), dann müsste ein Casting auf die Basisklasse erfolgen, in der Form ((Orbiter)p).Display(); bzw. so Orbiter po = p; po.Display(); Beachten Sie bitte auch, dass im Gegensatz zu C++ unter C# eine Mehrfachvererbung nicht möglich ist!
81
C# 82
2 Virtuelle Methoden Da sich immer wieder feststellen lässt, dass auch erfahrene C++-Programmierer sich mit dem Begriff virtuelle Methode schwer tun, wird dieses Feature an einem kleinen Beispiel demonstriert. public static void Main() { Orbiter o = new Orbiter("o",100.0M,0.0M,0.0M); Planet p = new Planet("Jupiter", 1000.0M,200.3M,235.45M,353.2M); Spaceship s = new Spaceship("Enterprise", 20.3M,31.5M,83.8M,1000M); Orbiter [] SpaceObjects; SpaceObjects = new Orbiter[3]; SpaceObjects[0] = o; SpaceObjects[1] = p; SpaceObjects[2] = s; for(int i = 0;i