Kapitel 5 Anatomie eines C#-Programms In diesem Kapitel: Programmaufbau using-Direktive und Framework-Klassen Dateien und Assemblies Imperative Programmierung in C# Objektorientierte Programmierung in C# 94 98 102 104 109 93 Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 94 Kapitel 5: Anatomie eines C#-Programms Als Einleitung und Vorbereitung zu den nachfolgenden Kapiteln, die sich eingehender mit den einzelnen Elementen und Konzepten der Programmiersprache C# beschäftigen, gibt dieses Kapitel einen Überblick über die Organisation von C#-Code, die typischen Elemente einer C#-Anwendung, die Verteilung des Codes auf mehrere Dateien und Assemblies und die Idee der objektorientierten Programmierung. Programmaufbau Anders als die Hybrid-Sprache C++, die gleichermaßen der strukturierten wie der objektorientierten Programmierung verbunden ist, gehört C# zu den rein objektorientierten Sprachen. Zwar kann sich der Programmierer auch in C#, so er will, den Idealen und Ideen der objektorientierten Programmierung weitgehend verschließen, indem er, statt Klassen für eigene Objekte zu definieren, allein mit statischen Methoden arbeitet, doch ganz entziehen kann er sich dem Diktat der Objekte und Klassen nicht, denn abgesehen davon, dass die bei der täglichen Programmierarbeit dringend benötigte Standardfunktionalität des .NET Framework in Form von Klassen bereitgestellt wird, verlangt C# vom Programmierer, dass dieser auch seinen eigenen Code in Klassen verpackt. 1 Eine C#-Anwendung ist letztes Endes also nichts anderes als eine Sammlung von Klassendefinitionen . Eine dieser Klassen muss einen Eintrittspunkt definieren: die Main()-Methode, mit der die Programmausführung beginnt. Selbst einfachste C#-Anwendungen bestehen also zumindest aus einer Klasse mit einer Main()-Methode. Nicht zwingend erforderlich, aber üblich sind using-Direktiven und Kommentare. Die nachfolgenden Abschnitte stellen die wichtigsten Programmelemente kurz vor. /* 4 * Demo-Programm * * Dateiname: HelloWorld.cs * Copyright: dieFirma.com */ 3 1 Klasse 2 Eintrittspunkt 3 using-Direktive 4 Kommentar using System; class Program 1 { static void Main(string[] args) 2 { // Anwender begrüßen 4 Console.WriteLine("Hallo Welt!"); } } Abbildung 5.1 Elemente eines typischen C#Programms Der Programmaufbau als Spiegel der eigenen Genealogie So wie die Entwicklung eines menschlichen Fötus die Entwicklung des Menschen widerspiegelt, so lässt sich am Layout eines C#-Programms die Evolution der imperativen Programmiersprache nachvollziehen. Am Beginn dieser Evolution standen einfache imperative Sprache wie Fortran (1954) oder Basic (1964), deren Programme aus einer Abfolge von simplen Anweisungen aufgebaut waren. 1 Eigentlich Typdefinitionen, denn neben Klassen können auch Strukturen, Schnittstellen und Enumerationen definiert werden. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 95 Programmaufbau Mit zunehmender Komplexität der Programme wuchs auch die Notwendigkeit, den Code zu modularisieren: Anweisungen, die häufig benötigte Teilaufgaben lösten (etwa die Ausgabe auf Konsole), wurden zu Funktionen zusammengefasst. Der in einer Funktion enthaltene Code konnte praktisch jederzeit und an beliebiger Stelle durch Aufruf der Funktion ausgeführt werden. Über Parameter konnte die Funktion beim Aufruf Daten vom Aufrufer entgegen nehmen, über ihren Rückgabewert konnte sie Ergebnisse zurückliefern. Imperative Sprachen, die das Funktionenkonzept unterstützen, werden als strukturierte Programmiersprachen bezeichnet. Ihr erfolgreichster Vertreter war die Sprache C, deren Programme alle mit einer Funktion main() begannen. In der strukturierten Programmierung sind die Daten und die sie verarbeitenden Funktionen voneinander getrennt. Diese Trennung aufzuheben und den Programmierer mit komplexen Objekten arbeiten zu lassen, die in sich Daten und Funktionen vereinen, war das Verdienst der objektorientierten Programmierung. Die erste objektorientierte Sprache war Simula (1967). Inspiriert von Simula erweiterte der Däne Bjarne Stroustrup Mitte der achtziger Jahre die Sprache C um objektorientierte Konzepte zur Sprache C++. Wegen der Abwärtskompatibilität zu C begannen C++-Programme immer noch mit der main()Funktion und der Programmierer konnte wahlweise mit Klassen und Objekten oder mit globalen Variablen und allein stehenden Funktionen arbeiten – oder beide Paradigmen vermischen. In C# wurde dieses Zwitterstadium beendet, globale Daten und allein stehende Funktionen wurden aufgegeben und aus der main()-Funktion wurde die in einer Klasse definierte statische Main()-Methode. Zudem übernahm C# – wie Java – von Smalltalk die Idee des virtuellen Zwischencodes und der automatischen Speicherbereinigung (Garbage Collection). using System; class Program { static void Main(string[] args) { int[] values = { 1, 5, -89 }; int sum; objektorientiert strukturiert imperativ sum = 0; for (int i = 0; i < values.Length; i++) sum = sum + values[i]; Console.WriteLine(sum); } } Abbildung 5.2 C#-Programmierung geschieht auf drei Ebenen, die den Paradigmen der imperativen, strukturierten und objektorientierten Programmierung zugeordnet werden können Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 96 Kapitel 5: Anatomie eines C#-Programms Klassen In C# ist das Konzept der Klasse praktisch allgegenwärtig: sei es als Element des Programmlayouts (siehe oben), als benutzerdefinierter Datentyp (siehe Kapitel 6 und 10) oder als Herzstück der objektorientierten Programmierung in C# (siehe den gleichnamigen Abschnitt weiter hinten in diesem Kapitel). Hier sei nur kurz angemerkt, dass Klassen Typen darstellen, die aus verschiedenen Elementen (Member) zusammen gesetzt sind. Die wichtigsten Member sind Felder (klasseninterne Variablen) und Methoden (klasseninterne Funktionen). Nach Verwendung und Bedeutung für die Klasse unterscheidet man statische und nichtstatische Member. Statische Member, die mit dem Schlüsselwort static definiert sind, können direkt über den Namen der Klasse aufgerufen werden: Klassenname.Member. Nicht-statische Member können nur über Objekte angesprochen werden. Objekte sind Instanzen einer Klasse, die mit Hilfe des Schlüsselworts new erzeugt werden. Bei der Instanzbildung erhält das neue Objekt eine Kopie von jedem (nicht-statischen) Feld der Klasse. Werden über ein Objekt (nicht-statische) Methoden der Klasse aufgerufen, operieren diese auf den Daten des Objekts. Nicht-statische Member werden auch als Instanzmember bezeichnet. Eintrittspunkt Main() Die Ausführung einer C#-Anwendung beginnt mit der Main()-Methode, die in C# eine der folgenden Signaturen haben muss: static static static static void Main() int Main() void Main(string[] args) int Main(string[] args) ACHTUNG Für Java-Programmierer: Methoden werden in C# üblicherweise groß geschrieben. Im Falle von Main() ist die Großschreibung sogar obligatorisch, ansonsten wird die Methode nicht als Eintrittspunkt in die Anwendung erkannt. Rückgabewert und Batch-Dateien Wenn Main() mit dem Rückgabewert int deklariert wird, muss sie mit einer return-Anweisung abschließen, die eine Ganzzahl zurückliefert. static int Main() { // ... return 0; } Dies ist immer dann interessant, wenn die Anwendung je nach Verlauf der Sitzung unterschiedliche Rückgabewerte liefert, die dann von DOS-Batch-Dateien, die die Anwendung aufrufen, ausgewertet werden können. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Programmaufbau 97 Parameter und Befehlszeilenargumente Ruft der Anwender ein Programm über die Konsole (Eingabeaufforderung) auf, kann er dem Programm direkt beim Aufruf Daten übergeben. Die Daten werden dazu einfach als Argumente hinter dem Programmnamen aufgelistet. Prompt:> Programmname 1 2 HINWEIS Befehlszeilenargumente können auch via Batch-Dateien oder Meta-Programme, die die Anwendungen aufrufen, übergeben werden. Die Visual Studio-IDE beispielsweise bietet auf der Seite Debuggen der Projekteigenschaften ein Eingabefeld Befehlszeilenargumente an, über das Sie Argumente an zu debuggende Anwendungen übergeben können. Um die Befehlszeilenargumente in der Anwendung in Empfang zu nehmen, müssen Sie Main() mit einem string[]-Parameter definieren. Die weitere Verarbeitung hängt von der Anwendung und der Bedeutung der Argumente für die Anwendung ab. Die meisten Anwendungen übernehmen über die Befehlszeile zu verarbeitende Daten (beispielsweise Dateinamen) oder Schalter zur Konfiguration des Programms. Gibt es obligatorische Befehlszeilenargumente prüft die Anwendung in der Regel als Erstes, ob die korrekte (Mindest-)Zahl Argumente übergeben wurden. Falls nicht, werden eine Fehlermeldung und ein Hinweis auf den korrekten Aufruf ausgegeben. Die folgende Demo-Anwendung prüft, ob Befehlszeilenargumente übergeben wurden. Wenn ja, werden die Argumente ausgegeben. using System; class Befehlszeile { static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("\n keine Befehlszeilenargumente! \n"); } else { Console.WriteLine("\n Befehlszeilenargumente: \n"); for (int i = 0; i < args.Length; i++) Console.WriteLine("\t {0}. Argument: {1} \n", i, args[i]); } } } Listing 5.1 Befehlszeile.cs C:\>Befehlszeile 1 2 drei vier Befehlszeilenargumente: 0. Argument: 1 1. Argument: 2 2. Argument: drei 3. Argument: vier Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 98 Kapitel 5: Anatomie eines C#-Programms TIPP Die Argumente der Befehlszeile werden durch Leerzeichen voneinander getrennt. Wenn Sie ein Argument übergeben wollen, das selbst Leerzeichen enthält, müssen Sie das Argument in Anführungszeichen setzen, beispielsweise C:\>Programmname "ein Argument" using-Direktive und Framework-Klassen Ohne die Funktionalität der .NET Framework-Klassen ist eine Programmierung mit C# kaum denkbar, ja schlichtweg unmöglich. Ob Sie Daten auf die Konsole ausgeben, die Uhrzeit abfragen, grafische Benutzeroberflächen erstellen oder mehrere Threads parallel ablaufen lassen möchten… für nahezu alle wichtigen Programmieraufgaben finden Sie im .NET Framework vordefinierte Klassen, auf deren Funktionalität Sie zurückgreifen können. Die Klassen der .NET Framework-Bibliothek sind auf mehrere DLL-Assemblies verteilt (System.dll, System.Data.dll, System.Windows.Forms.dll, System.XML.dll …). Um Klassen aus diesen Assemblies verwenden zu können, müssen Sie Ihren C#-Code zusammen mit Verweisen auf die Assemblies kompilieren. In Visual Studio richten Sie Verweise über die Kontextmenübefehle des gleichnamigen Projektunterknotens ein. Wenn Sie direkt mit dem csc-Compiler arbeiten, benutzen Sie die Compiler-Option /r, um auf die betreffenden DLLs zu verweisen. Die DLL System.dll müssen Sie nicht explizit auflisten. In Ihrem Quelltext können Sie die Klassen des .NET Framework direkt benutzen, müssen aber beachten, dass die Klassen in eine hierarchische Struktur von Namespaces organisiert sind. Sie müssen daher entweder: dem Klassennamen den vollständigen Namespace-Pfad voranstellen class Program { static void Main(string[] args) { System.Console.WriteLine("Hallo Welt!"); } } oder am Anfang des Quelltextes eine using-Direktive einfügen, die alle Namen aus dem gewünschten Namespace verfügbar macht using System; class Program { static void Main(string[] args) { Console.WriteLine("Hallo Welt!"); } } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 99 using-Direktive und Framework-Klassen ACHTUNG Für C++- und Java-Programmierer: C# verfügt über keine eigene Standardbibliothek. Stattdessen bilden die Klassen des .NET Framework die C#-Laufzeitbibliothek. Anders als bei Java wird der System-Namespace (vergleichbar java.lang) nicht automatisch eingebunden. Ein- und Ausgabe In Windows Forms-Anwendungen verlaufen Kommunikation und Datenaustausch mit dem Anwender nahezu ausschließlich über die Steuerelemente der grafischen Benutzeroberfläche. Konsolenanwendungen besitzen hingegen keine eigene Benutzeroberfläche, sie nutzen für den Austausch mit dem Anwender die Konsole. Ausgabe Für die Ausgabe auf die Konsole stellt die .NET Framework-Klasse System.Console die statische Methode WriteLine() zur Verfügung, der Sie einfach den auszugebenden String übergeben: Console.WriteLine("Hallo Welt!"); Mit der gleichen Methode können Sie auch Werte der vordefinierten primitiven Typen (int, double etc.) oder Objekte ausgeben. Die Werte werden dabei automatisch in ihre String-Darstellung umgewandelt: double number = 0.234; Console.WriteLine(number); Strings und Variablenwerte können bei der Ausgabe mit dem +-Operator kombiniert werden: double zahl = 0.234; Console.WriteLine("Wert von number: " + number); Oder Sie fügen in den auszugebenden String durchnummerierte Platzhalter der Form {0}, {1} und so weiter ein, die WriteLine() bei der Ausgabe durch die Werte der nachfolgenden Argumente ersetzt: Console.WriteLine("Das Quadrat von {0} ist {1}", number, number*number); Platzhalter bieten zudem die Möglichkeit, das Ausgabeformat von numerischen Werten zu beeinflussen. Mehr zu diesem Thema in Kapitel 22, Abschnitt »Formatierung mit Platzhaltern«. Console.WriteLine("Wert von n: {0:D}", n); Console.WriteLine("Wert von n: {0:D2}", n); Console.WriteLine("Wert von n: {0:F2}", n); Console.WriteLine("Preis: {0:C2}", n); // // // // // // // Ausgabe als ganze Zahl Ausgabe als ganze Zahl (mindestens 2 Zeichen, notfalls Leerzeichen) Ausgabe als Dezimalzahl mit genau 2 Nachkommastellen Ausgabe als Währungsangabe mit genau 2 Nachkommastellen Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 100 Kapitel 5: Anatomie eines C#-Programms Wenn Sie die Ausgabe nicht mit einem Zeilenumbruch beenden wollen, verwenden Sie statt WriteLine() die Methode Write(): Console.Write(" Geben Sie eine Zahl ein: "); Eingabe Zum Einlesen von Benutzerdaten verwenden Sie die statische Console-Methode ReadLine(): 1. Vor dem Einlesen der Benutzerdaten sollten Sie einen Text ausgeben, der dem Benutzer mitteilt, welche Art von Daten er jetzt eingeben soll. 2. Lesen Sie die Daten dann durch einen Aufruf von Console.ReadLine() ein und speichern Sie die Daten in einer Variablen. 3. Benutzereingaben werden immer als Strings eingelesen. Enthalten diese Strings Zahlendarstellungen, können Sie diese gleich mit Hilfe einer der Convert-Methoden ToInt32(), ToLong(), ToDecimal(), ToDouble() etc. umwandeln und in numerischen Variablen abspeichern. string name = ""; int age = 0; Console.Write(" Geben Sie Ihren Namen ein: "); name = Console.ReadLine(); // 1 // 2 Console.Write(" Geben Sie Ihr Alter ein: "); string input = Console.ReadLine(); age = Convert.ToInt32(input); // 1 // 2 // 3 Console.WriteLine(""); Console.WriteLine(" {0} ist {1} Jahre alt", name, age); Schritt 2 und 3 können auch in einem Schritt ausgeführt werden: age = Convert.ToInt32(Console.ReadLine()); Wenn Sie eine Stringeingabe in eine Zahl zu verwandeln suchen, die keine korrekte Zahlendarstellung enthält, wird eine FormatException-Ausnahme ausgelöst, die – wenn nicht behandelt – zum Programmabsturz führt. Benutzerfreundlicher ist es, die Ausnahme im Programm abzufangen und dieses mit einem entsprechenden Hinweis zu beenden (oder gegebenenfalls weiterzuführen). try { Console.Write(" Geben Sie Ihr Alter ein: "); string input = Console.ReadLine(); age = Convert.ToInt32(input); } catch (FormatException) { Console.Write(" Die Eingabe hatte kein gültiges Zahlenformat "); Environment.Exit(0); } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 101 using-Direktive und Framework-Klassen Kommentare C# kennt drei Arten von Kommentaren, mit denen Quelltext für die Nachwelt oder spätere Überarbeitungen kommentiert und erläutert werden kann. Die folgenden Abschnitte stellen die drei Möglichkeiten zur Kommentierung von Quelltext vor. Einzeilige Kommentare //-Kommentare reichen bis zum Ende der aktuellen Zeile. Sie werden meist genutzt, um die Bedeutung von Variablen oder einzelnen Code-Abschnitten anzugeben int AMethod() { double capital; int interestRate; int t; // Startkapital // Verzinsung in Prozent // Laufzeit // Kapitalertrag berechnen double result = capital * (1 + interestRate/100.0 * t); // ... Mehrzeilige Kommentare Mehrzeilige Kommentare werden mit /* eingeleitet und enden mit */. Sie können nicht ineinander verschachtelt werden. /*…*/-Kommentare werden traditionell für ausführlichere Beschreibungen von Klassen, Strukturen, Methoden etc. verwendet, obwohl es auch Programmierer gibt, die für diese Aufgaben ebenfalls //-Kommentare nutzen und die /*…*/-Kommentare ausschließlich zum Auskommentieren von Quelltextblöcken nutzen. In C# haben die mehrzeiligen Kommentare zusätzliche Konkurrenz bekommen, denn für die Beschreibung von Klassen und Klassenmembern empfiehlt sich die Verwendung der speziellen Dokumentationskommentare. /* * Demo-Programm * * Dateiname: HelloWorld.cs * Autor: Manfred Mustermann */ using System; class Program { } Dokumentationskommentare XML-Dokumentationskommentare beginnen mit einem dreifachen Schrägstrich /// und erläutern nicht nur den Quelltext, sondern können auch zur Erstellung von HTML- oder XML-Dokumentationen genutzt werden. Den XML-Dokumentationskommentaren ist das Kapitel 21 gewidmet. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 102 Kapitel 5: Anatomie eines C#-Programms Dateien und Assemblies Der Quelltext einer C#-Anwendung besteht zu 99% aus Typdefinitionen, allen voran Klassen. Gespeichert wird dieser Quelltext in Quelldateien mit der Dateierweiterung .cs. Welche Beziehung besteht zwischen Klassen (allgemein Typdefinitionen) und den Quelltextdateien? Klassen und Dateien Grundsätzlich sollte jede Klasse in ihrer eigenen Quelltextdatei definiert werden. Auf diese Weise wird der Quelltext automatisch übersichtlich organisiert und die vom Programmierer gewählte CodeModularisierung spiegelt sich in der Aufteilung auf die Dateien wider. Sie sind allerdings nicht sklavisch an die Einhaltung dieser Regel gebunden. In C# können Sie genauso gut beliebig viele Klassen (und andere Typen) in einer Quelltextdatei zusammenfassen, ja Sie können sogar die Definition einer Klasse auf mehrere Dateien verteilen (letzteres ist erst ab C# 2.0 möglich; siehe in Kapitel 10 den Abschnitt »Klassendefinition«). Sie sollten allerdings einen Grund für die Abweichung von der 1:1Regel haben. Visual Studio beispielsweise nutzt die Möglichkeit zur Verteilung des Klassencodes auf mehrere Dateien dazu, den Code von Formular-Klassen sauber zu trennen: in Code, der vom Windows Forms-Designer erstellt und verwaltet wird (Form1.cs-Datei), und in Code, den der Programmierer selbst editiert (Form1.Designer.cs-Datei). Für den normalen Entwickler dürfte die Option, mehrere Klassen (allgemein Typen) in einer Quelltextdatei zusammen zu fassen, interessanter sein – beispielsweise um Hilfsklassen (typen), die nur zur Unterstützung der Hauptklasse der Datei benötigt werden, zusammen mit dieser in einer Datei definieren zu können (falls die Hilfstypen nicht sowieso als verschachtelte Typen in der Klasse definiert werden; siehe in Kapitel 10 den Abschnitt »Verschachtelte Typdefinitionen«). ACHTUNG Für Java-Programmierer: Anders als in Java können in einer C#-Quelltextdatei mehrere public-Klassen definiert werden. Entsprechend gibt es keine Vorschrift, dass der Dateiname gleich dem Namen einer der Klassen aus der Datei sein müsste. Dateien und Assemblies Der üblichen 1:1-Zuordnung von Klassen zu Dateien steht die n:1-Relation zwischen Quelldateien und Assemblies gegenüber. Mit anderen Worten: Bei der Kompilierung muss der Programmierer den Quelltext, den er der gerade erst aus gutem Grund auf mehrere Dateien verteilt hat, wieder zusammenführen. In Visual Studio geschieht dies natürlich mithilfe der Projektverwaltung (siehe in Kapitel 2 den Abschnitt »Die Projektverwaltung«). Die Projektverwaltung kompiliert die Quelltextdateien allerdings nicht selbst, sondern generiert lediglich aus den Daten über Projektaufbau und -konfiguration den passenden Aufruf des Befehlszeilencompiler csc.exe. Wenn Sie möchten, können Sie den csc-Compiler natürlich auch direkt aufrufen, um Ihre Quelldateien zu Assemblies zu kompilieren. Die Bedienung des Compilers ist nicht sonderlich schwierig und verrät gleichzeitig ein bisschen mehr über die Arbeitsweise der Projektverwaltung. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Dateien und Assemblies 103 Anwendungen aus einzelnen Dateien erstellen Eine einfache Anwendung, die nur aus einer einzigen Quelltextdatei besteht, kompilieren Sie, indem Sie csc mit dem Namen der Quelltextdatei aufrufen: csc Quelltextdatei.cs Der Compiler erzeugt daraufhin eine Assembly für eine Konsolenanwendung. Die Assembly trägt den Namen der Quelltextdatei – also Quelltextdatei.exe – und wird im aktuellen Verzeichnis abgespeichert. Soll die .exe-Datei einen anderen Namen erhalten, geben Sie diesen als Wert der /out-Option an: csc /out:Demo.exe Quelltextdatei.cs Bei der Arbeit mit Visual Studio würden Sie stattdessen ein Projekt auf Basis der Projektvorlage für Konsolenanwendungen anlegen und den Quelltext in die Datei Program.cs eingeben, die zur Grundausstattung der Projektvorlage gehört. Beim Erstellen wird eine .exe-Datei erzeugt, die den Namen des Projekts (nicht der Quelltextdatei!) trägt. Anwendungen aus mehreren Dateien erstellen Gehören zu der Anwendung mehrere Quelltextdateien, müssen Sie diese alle im csc-Aufruf auflisten: csc Datei1.cs Datei2.cs Datei3.cs Liegen die Quelltextdateien in einem gemeinsamen Verzeichnis, kann der *-Platzhalter viel Tipparbeit sparen. Der folgende Aufruf kompiliert alle .cs-Dateien im aktuellen Verzeichnis zu einer Assembly: csc *.cs Die Assembly trägt den Namen der Quelltextdatei, die die Main()-Methode enthält. Bei der Arbeit mit Visual Studio müssen Sie darauf achten, dass alle zur Anwendung gehörenden Quelltextdateien auch als Teil des Projekts erfasst sind. Neue Quelltextdateien legen Sie dazu direkt als Teil des Projekts an (Menübefehl Projekt/Neues Element hinzufügen); bestehende Quelltextdateien können Sie mit dem Menübefehl Projekt/Vorhandenes Element hinzufügen in das Projekt integrieren. Bibliotheken und andere Zieltypen erstellen Insgesamt können Sie Assemblies für vier verschiedene Zieltypen erstellen: Konsolenanwendungen, Windows Forms-Anwendungen, Bibliotheken und Module. Den Zieltyp teilen Sie csc über die Option /target, abgekürzt /t, mit. Mögliche Werte sind: exe (Standard), winexe, library und module. csc /t:library Class1.cs Class2.cs Wenn Sie eine Bibliothek erstellen, trägt diese standardmäßig den Namen der ersten aufgeführten Quelldatei mit der Dateierweiterung dll – in obigem Fall also Class1.dll. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 104 Kapitel 5: Anatomie eines C#-Programms Soll die Bibliothek einen anderen Namen erhalten, geben Sie diesen als Wert der /out-Option an: csc /t:library /out:mylib.dll Class1.cs Class2.cs Unter Visual Studio legen Sie den Zieltyp durch Auswahl einer passenden Projektvorlage fest. Man sollte es zwar vermeiden, aber falls Sie den Zieltyp nachträglich ändern müssen, finden Sie dazu in den Projekteigenschaften auf der Seite Anwendung die Option Ausgabetyp. Bibliotheken in Anwendungen verwenden Wenn Sie eine Anwendung kompilieren, die auf Klassen und andere Typen zugreift, die in einer anderen Assembly definiert sind, müssen Sie dem Compiler einen Verweis auf diese Assembly mitgeben. Hierzu gibt es die Compiler-Option /reference oder abgekürzt /r: csc /t:winexe /r:mylib.dll Program.cs Form1.cs Unter Visual Studio richten Sie Verweise mithilfe des Befehls Verweis hinzufügen aus dem Kontextmenü des Projektunterknotens Verweise ein, siehe auch in Kapitel 2 den Abschnitt »Die Projektverwaltung«. Imperative Programmierung in C# Das imperative Erbe von C# besteht im Wesentlichen aus vier grundlegenden Konzepten: Datentypen, Variablen, Operatoren und Anweisungen. Daten und Datentypen Damit ein Programm Daten eines bestimmten Typs (ganze Zahlen, Dezimalzahlen, Zeichenfolgen, Adressen, Vektoren etc.) verarbeiten kann, muss der jeweilige Datentyp bekannt sein. Der Typ bestimmt, wie die Daten im Arbeitsspeicher abgelegt werden und welche Operationen auf den Daten erlaubt sind. C# kennt verschiedene vordefinierte Typen, beispielsweise int für Ganzzahlen, double für Gleitkommazahlen (Dezimalzahlen), bool für die booleschen Wahrheitswerte (true und false) oder string für Strings (Zeichenfolgen). Weitere Datentypen, insbesondere für komplexe Daten wie Adressen, Vektoren, Kunden etc., können in Form von Klassen, Strukturen oder Enumerationen vom Programmierer definiert werden. Die Daten eines Typs werden als Werte bezeichnet, im Falle komplexer Typen, insbesondere Klassen, auch als Objekte. Die Typinformation ist vor allem für die Übersetzungswerkzeuge wichtig, also für Compiler und Interpreter. Für den Programmierer weit interessanter ist hingegen die Frage, wie er die Daten, mit denen er arbeiten möchte, überhaupt in seinem Programm repräsentieren kann. Dies führt uns zu den Literalen und Variablen. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 105 Imperative Programmierung in C# Literale und Variablen Literale sind Daten, die direkt im Quelltext codiert sind. Es gibt sie nur für einige vordefinierte Typen wie Ganzzahlen, Gleitkommazahlen oder Strings. Der Typ eines Literals ist am Format erkennbar 1, -12 10.34 "Hallo" // int-Literal // double-Literal // string-Literal Variablen sind Speicher für Daten. Sie werden unter Angabe eines Typs und eines Namens deklariert. int age; string name; double price; Mit dem Zuweisungsoperator = können in Variablen Werte gespeichert werden. Voraussetzung ist allerdings, dass der Typ des Werts und der Typ der Variablen identisch sind oder es zumindest eine implizite Typumwandlung zwischen den Typen gibt. age = 34; name = "Jim"; price = 10.34; // implizite Umwandlung von double (Typ des Literals) zu int (Typ der // Variablen price). In price wird der umgewandelte Wert (10) // gespeichert. In anderen, allerdings bei weitem nicht allen Fällen ist eine explizite Typumwandlung mit dem CastOperator () oder durch Aufruf einer passenden Konvertierungsmethode möglich: double r = 34.5; int age = (int) r; // explizite Typumwandlung, speichert in age // den Wert 34 string s = "23.95"; double price = Convert.ToDouble(s); // Konvertierungsmethode, speichert in price // den Wert 23.95 ACHTUNG Für C++-Programmierer: In C# fallen Deklaration (Bekanntmachung eines neuen Elements beim Compiler) und Definition (Beschreibung des Elements) stets zusammen. Die Begriffe Deklaration und Definition werden daher meist synonym verwendet. Operatoren und Ausdrücke Die Werte der primitiven vordefinierten Typen können mit Operatoren verarbeitet werden. Ganzzahlen können beispielsweise mit den Operatoren +, –, * und / addiert, subtrahiert, multipliziert oder dividiert werden: 3 + 4 // addiert die ganzen Zahlen Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 106 Kapitel 5: Anatomie eines C#-Programms Eine Kombination aus Literalen, Variablen und Operatoren bezeichnet man als Ausdruck. Variablen in Ausdrücken repräsentieren den Wert, der in ihnen gerade gespeichert ist. Jeder Ausdruck steht für einen Wert, der zur Laufzeit berechnet wird. 3 + 4 3 + 4 * 2 n < 4 "Leopold" + "Bloom" // // // // Wert 7 Wert 11 true, wenn der Wert von n kleiner als 4 ist, ansonsten false verkettet die beiden Strings zu "Leopold Bloom" HINWEIS Grundsätzlich verändern Operatoren nicht den Wert ihrer Operanden. Einzige Ausnahme: die Operatoren für Inkrement ++ und Dekrement --, die den Wert ihres einzigen Operanden um Eins erhöhen oder erniedrigen. Anweisungen Anweisungen legen fest, was eine Anwendung tun soll. Es gibt verschiedene Arten von Anweisungen, die meisten von Ihnen erkannt man daran, dass sie mit einem Semikolon abgeschlossen werden. Zwei Arten von Anweisungen haben Sie bereits kennen gelernt: die Deklarationsanweisung zur Einrichtung von Variablen und die Zuweisung. int int int c = a = 10; b = 5; c; a * b; // Wird einer Variablen bereits bei der Definition ein Wert zugewiesen // spricht man von Initialisierung // Zuweisung Auf der rechten Seite der Zuweisung steht eine Variable, auf der linken Seite ein Ausdruck. Blöcke Anweisungen können mithilfe geschweifter Klammern zu Anweisungsblöcken zusammen gefasst werden: { // Folge von Anweisungen } Blöcke können ineinander geschachtelt werden. Variablen, die in einem Block definiert werden, sind nur in ihrem Block gültig (untergeordnete Blöcke mit eingeschlossen). Verzweigungen Speziellen Anweisungen dienen dazu, den Programmfluss zu steuern. Dieser verläuft standardmäßig ganz geradlinig, d.h. die Anweisungen werden in der Reihenfolge ausgeführt, in der sie im Quelltext stehen. Sprünge, Verzweigungen und Schleifen erlauben dem Programmierer den Programmablauf zu steuern. Die if-Anweisung ist eine einfache Verzweigung. Sie prüft anhand einer Bedingung (ein Ausdruck, der einen der booleschen Wahrheitswerte true oder false zurückliefert), ob die nachfolgende Anweisung respektive der nachfolgende Anweisungsblock ausgeführt werden soll. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Imperative Programmierung in C# 107 if (n < 100) { //... Anweisungen, die nur dann ausgeführt werden, wenn n kleiner 100 ist } Die Erweiterung der if-Anweisung ist die if…else-Anweisung, die anhand einer Bedingung entscheidet, welcher von zwei nachfolgenden Blöcken ausgeführt werden soll. if (n < 100) { //... Anweisungen, die ausgeführt werden, wenn n kleiner 100 ist } else { //... Anweisungen, die ausgeführt werden, wenn n nicht kleiner 100 ist } Schleifen Schleifen sind Iterationsanweisungen, die einen zugehörigen Anweisungsblock mehrmals hintereinander ausführen. Die wichtigsten Schleifen sind die while- und die for-Schleife. Die while-Schleife wird so lange ausgeführt, wie die eingangs der Schleife formulierte Bedingung true ergibt. int n = 2; while (n < 10) { n = n + (n-1); } Schleifen bergen stets die Gefahr, dass es durch Unachtsamkeit oder Fehleinschätzung zu Endlosausführungen kommt. Die obige Schleife würde beispielsweise nach dem vierten Durchlauf (Iteration) beendet, da n dann den Wert 17 enthält. Würde n aber mit dem Wert 1 initialisiert, würde die Schleife endlos ausgeführt, da der Ausdruck n + (n-1) immer wieder 1 ergibt. Die for-Schleife ist etwas sicherer in der Verwendung, da in ihrem Fall alle Informationen zur Schleifensteuerung im Kopf der Schleife zusammengezogen sind. Sie wird meist dann verwendet, wenn die Anzahl der Schleifeniterationen feststeht. Die folgende for-Schleife beispielsweise berechnet das Quadrat der ersten zehn natürlichen Zahlen: for (int i = 1; i <= 10; i++) { Console.WriteLine(" Das Quadrat von {0} ist {1}", i, i*i); } Die Ausgabe sieht folgendermaßen aus: Das Quadrat von 1 ist 1 Das Quadrat von 2 ist 4 Das Quadrat von 3 ist 9 Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 108 Kapitel 5: Anatomie eines C#-Programms Das Das Das Das Das Das Quadrat Quadrat Quadrat Quadrat Quadrat Quadrat von von von von von von 4 5 6 7 8 9 ist ist ist ist ist ist 16 25 36 49 64 81 Funktionen (Methoden) Höher entwickelte imperative Programmiersprachen unterstützen die Auslagerung von Code in Funktionen. So können Teilprobleme separat gelöst und anschließend an anderen Stellen im Quelltext beliebig eingesetzt werden. C# kennt Funktionen nur als Member von Klassen (dann Methoden genannt). Das Prinzip ist allerdings das Gleiche. Funktionen bestehen aus einem Funktionskopf und einem Anweisungsblock (Funktionsrumpf). Der Funktionskopf deklariert die Funktion, gibt ihr einen Namen und legt die Schnittstelle zur Außenwelt fest. Eine Funktion zur Berechnung des Quadrats könnte wie folgt aussehen: int Quadrat(int n) { int result; result = n * n; return result; } Es ist sinnvoll, den Funktionsnamen so zu wählen, dass daraus der Gebrauch der Funktion direkt ersichtlich ist. Hinter dem Funktionsnamen folgt in Klammern die Liste der Parameter, die der Funktion übergeben werden können. Im obigen Fall ist für die Funktion nur ein Parameter (n) vorgesehen. Mittels der Anweisung return result; wird das Ergebnis der Berechnung zurückgeliefert. Das Schlüsselwort int vor dem Funktionsnamen gibt an, dass es sich bei dem von der Funktion zurückgelieferten Wert um einen int-Wert handelt. Code, der die Funktion aufruft, übergibt ihr Werte für die Parameter und speichert das Ergebnis in einer eigene Variable: int square = 0; int number = 12; square = Quadrat(number); HINWEIS Es ist angebracht, an diese Stelle einige weitere mit Funktionen verbundene Begriffe zu erklären: Als Parameter einer Funktion werden diejenigen Variablen der Funktion bezeichnet, die innerhalb der Klammern im Funktionskopf deklariert werden Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 109 Die Werte, die beim Funktionsaufruf an die Parameter übergeben werden, nennt man auch Argumente. Variablen, die innerhalb eines Funktionsrumpfes deklariert werden, bezeichnet man als lokale Variablen. Objektorientierte Programmierung in C# Objektorientierte Programmierung bedeutet, dass der Programmierer versucht, die ihm gestellten Aufgaben mithilfe von Objekten zu lösen – eine Form der Problemlösung, die uns Menschen unter anderem den Titel »Werkzeug-Benutzer« eingebracht hat. Stellen Sie sich ein Grundstück vor, das von einem kleinen Bach durchflossen wird. Von diesem Bach wollen Sie einen Kanal abzweigen, der zu einem Feld führt, das Sie auf diese Weise bewässern möchten. Wie gehen Sie vor? Sie könnten den Kanal mit Ihren Händen graben. Diese Lösung verzichtet auf jegliche Hilfsmittel. Übertragen auf die Software-Entwicklung würde dies bedeuteten, dass Sie auf die Vorzüge einer höheren Programmiersprache verzichten und in Assembler auf Maschinenbefehl-Ebene programmieren. In der Software-Entwicklung hat dieser Ansatz durchaus Vorzüge, führt allerdings hier wie dort zu blutigen Händen. Oder Sie benutzen eine Schaufel. Sie akzeptieren einfache Hilfsmittel, erledigen aber die ganze Arbeit in Handarbeit. In der Software-Entwicklung wäre dies mit dem Einsatz einer strukturieren, aber nicht objektorientierten höheren Programmiersprache vergleichbar. Sie könnten aber auch den Bauplan für einen kleinen Bagger entwerfen, den Bagger konstruieren und mit diesem den Kanal ausheben. Dies entspräche dem objektorientierten Ansatz. Statt alles in Handarbeit zu machen, überlegen Sie sich, wie ein Hilfsmittel (im OOP-Ansatz ein Objekt) aussehen müsste, mit dem Sie die gestellte Aufgabe schnell und effizient erledigen können. Da Sie kein solches Hilfsmittel zur Verfügung haben, entwerfen Sie einen Bauplan (im OOP-Ansatz eine Klasse), der beschreibt, wie das Hilfsmittel (Objekt) auszusehen hat. Dann konstruieren Sie nach dem Bauplan das Hilfsmittel (Objekterzeugung). Ist das Hilfsmittel fertig, benutzen Sie es zur Lösung der Aufgabe. In einem Aspekt stimmt die Korrelation zwischen technischem und objektorientiertem Lösungsansatz allerdings nicht: Während bei dem technischen Lösungsansatz der zweite Schritt (der Bau des Baggers) die meiste Zeit und Mühe kostet, kann sich der Programmierer darüber freuen, dass dieser Schritt (die Objekterzeugung) fast vollständig vom Compiler übernommen wird. Die Relation zwischen dem ersten und dritten Schritt ist hingegen durchaus realistisch wiedergegeben: Bei der objektorientierten Programmierung fließt viel Arbeit in die Konstruktion der Klassen (Baupläne). Die Programmierung mit den Objekten der Klassen fällt dafür umso leichter. Wie sind OOP-Objekte beschaffen? Die Objekte, mit denen wir es in der objektorientierten Programmierung zu tun haben, sind den Objekten, denen wir in der realen Welt begegnen, durchaus vergleichbar. Sie besitzen Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 110 Kapitel 5: Anatomie eines C#-Programms Merkmale Die Merkmale beschreiben das Objekt und seinen Zustand. Vielleicht haben Sie gerade eine Tasse vor sich stehen. Wenn Sie die Tasse anblicken, registriert ihr Gehirn sofort eine ganze Reihe von Merkmalen wie Größe, Form, Inhaltsvolumen, Farbe oder auch Merkmale, die den aktuellen Zustand beschreiben (etwa Lackschäden oder verbliebene Menge Kaffee in der Tasse). und Verhaltensweisen Die Verhaltensweisen geben an, wie das Objekt zu verwenden ist (aus einer Tasse kann man trinken) oder wie das Objekt selbst reagieren kann (eine Tasse kann zerspringen). Mit »Verhaltensweisen« verbinden wir in der Regel Aktivitäten, die ein Objekt von selbst zeigt: etwa das Wachsen einer Pflanze, das Wiehern eines Pferdes, das Umkippen einer Flasche oder das Klingeln des Weckers. Im objektorientierten Sinne zählen zu den Verhaltensweisen aber auch Aktivitäten, die mit der Bedienung oder Verwendung eines Objekts zu tun haben, also das Gießen einer Pflanze, das Pflegen eines Pferdes, das Öffnen einer Flasche oder das Stellen eines Weckers. Arten von Objekten Die Nachbildung realer Dinge durch Objekte ist ein wichtiges und mächtiges Instrument der objektorientierten Programmierung. Auf der anderen Seite würde die objektorientierte Programmierung schnell an ihre Grenzen stoßen, wenn ihre Objekte ausschließlich Dinge der realen Welt nachbilden könnten. Glücklicherweise kann ein Objekt aber nahezu alles sein, was als Einheit aus Merkmalen und zugehörigen Verhaltensweisen ausgedrückt werden kann. So gibt es beispielsweise Objekte, die Dinge der realen Welt nachbilden. Objekte, die virtuelle Dinge repräsentieren (beispielsweise Dinge, die auf dem Bildschirm zu sehen sind, wie Fenster oder Schaltflächen oder ein gezeichnetes Monster). Objekte, die Daten repräsentieren oder verwalten (ein Programm zur Verwaltung von Musik-CDs könnte die einzelnen CDs als Objekte darstellen und zusammen in einem Container-Objekt verwalten, das das Ablegen, Sortieren und Suchen bestimmter CDs erleichtert). Objekte, die einfach eine bestimmte Funktionalität zur Verfügung stellen (beispielsweise für einen gegebenen Satz von Daten statistische Berechnungen wie Mittelwert, Standardabweichung u.a. ausführen). Objekte und Klassen OOP-Objekte kommen nicht aus dem Nichts, sondern werden aus Klassen erzeugt. Die Klassendefinition Klassen sind nichts anderes als vom Programmierer definierte Datentypen. In der Klassendefinition werden die Merkmale und Verhaltensweise des Objekts zusammengefasst. Merkmale werden dabei als Variablen, Verhaltensweisen als Methoden definiert. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 111 Objektorientierte Programmierung in C# HINWEIS Die objektorientierte Terminologie ist etwas uneinheitlich. Variablen, die als Member von Klassen definiert werden, heißen in der Literatur meist Felder, Member-Variablen oder Elementvariablen. Methoden, die in Klassen definiert werden, werden auch als Member-Funktionen oder Elementfunktionen bezeichnet. Eine Klasse zur Erzeugung von Employee-Objekten könnte beispielsweise über die Felder Name und Salary sowie die Methode GetsPromotion() verfügen: class Employee { public string Name; public int Salary; public void GetsPromotion(int amount) { Salary = Salary + amount; } // Felder für Merkmale // Methode für Verhaltensweise } Klassen und Objekte Die Beziehung zwischen Klasse und Objekt sollte ganz klar sein. Im objektorientierten Denkmodell ist ein Objekt ein Ding, beispielsweise Sie selbst, Ihr Nachbar, der Bleistift, der neben Ihnen liegt, oder die Bäume, die Sie sehen, wenn Sie aus dem Fenster schauen. Einerseits ist jedes dieser Objekte für sich genommen einzigartig, andererseits gibt es zu jedem dieser Objekte auch gleichartige Objekte. Wenn Sie beispielsweise in ein Schreibwarengeschäft gehen, um einen Buntstift zu kaufen, finden Sie dort rote, blaue, grüne und gelbe, dicke und dünne, billige und teure Buntstifte. Zur Bezeichnung gleichartiger Objekte verwenden wir in der Sprache Oberbegriffe: »Buntstift« ist beispielsweise ein solcher Oberbegriff. Er weckt in uns die Vorstellung von einem langen, dünnen Schreibgerät aus Holz, das eine bestimmte Farbe und eine bestimmte Dicke hat, das man anspitzen und mit dem man malen kann. Beachten Sie, dass der Oberbegriff »Buntstift« nichts über die genaue Farbe oder Dicke aussagt, er legt nur fest, dass die Objekte, die wir als Buntstifte ansehen, Merkmale wie Farbe und Dicke haben. Welche Farbe und welche Dicke ein Buntstift-Objekt hat, ist seine Sache. Was in der Sprache die Oberbegriffe sind, sind in der objektorientierten Programmierung die Klassen. Auch Klassen sind allgemeine Beschreibungen für eine Gruppe gleichartiger Objekte. Während wir in der Sprache Oberbegriffe jedoch meist nur dazu verwenden, die Objekte, denen wir täglich begegnen, zu beschreiben und zu klassifizieren, erfüllen die Klassen in der objektorientierten Programmierung zwei Aufgaben: sie beschreiben die Dinge, die wir in dem Programm verwenden wollen und sie dienen als Vorlage oder Gussform, aus der wir die Objekte, mit denen wir arbeiten wollen, erst erzeugen. Die Objekterzeugung Eine Klasse ist lediglich ein Datentyp. Der nächste Schritt besteht folglich darin, eine Variable vom Typ der Klasse zu definieren. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 112 Kapitel 5: Anatomie eines C#-Programms Employee jim; Dies ist allerdings erst einmal nur eine Variable. Die Variable enthält noch kein Objekt. Welches Objekt auch, es wurde ja noch gar kein Objekt erzeugt. Dies geschieht erst vermittels des new-Operators: jim = new Employee(); Der new-Operator sorgt dafür, dass im Speicher ein Objekt, man spricht auch von einer Instanz, der Klasse angelegt wird. Jedes Objekt erhält dabei Kopien der in der Klasse definierten Felder und einen Verweis auf die Methoden der Klassen. HINWEIS Die einzelnen Objekte, die von einer Klasse gebildet werden, besitzen also jede einen eigenen Satz von Feldern, verwenden aber die gleichen Methoden. Die Einrichtung eines Objekts wird auch als Instanzbildung oder Instanziierung bezeichnet. Klassen sind Verweistypen Das mithilfe von new erzeugte Objekt wird allerdings nicht direkt in der Variablen jim gespeichert. Vielmehr wird das Objekt irgendwo im Arbeitspeicher angelegt und in der Variablen wird lediglich ein Verweis auf das Objekt gespeichert. Den Verweis liefert der new-Operator zurück. Oft werden Variablendefinition und Objekterzeugung in einem Schritt ausgeführt: Employee jim = new Employee(); HINWEIS C# teilt seine Typen in Wert- und Verweistypen, je nachdem, ob die Werte des Typs direkt in den Variablen gespeichert werden oder ob diese lediglich Verweise auf das irgendwo sonst im Speicher liegende Objekt verwahren. Die vordefinierten Typen int und double sind Beispiele für Werttypen, Klassen sind Verweistypen. Der Konstruktor Sind Ihnen im obigen Abschnitt die runden Klammern hinter dem Typ Employee aufgefallen? Es sieht aus, als wäre an der Objekterzeugung eine Methode beteiligt, die den Namen der Klasse trägt. Nun, dies ist gar nicht mal so falsch. Tatsächlich verfügt jede Klasse über eine spezielle »Methode«, den so genannten Konstruktor, der genauso heißt wie die Klasse. Klassen, die keinen eigenen Konstruktor definieren, bekommen einen Ersatzkonstruktor vom Compiler zugeteilt. Der Konstruktor wird ausschließlich bei der Objekterzeugung aufgerufen. Programmieren mit Objekten Mit Hilfe des Punktoperators können Sie auf die Member eines Objekts zugreifen: Employee jim = new Employee(); jim.Name = "Jim Bubble"; jim.Salary = 1500; Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 113 Console.WriteLine("\n Mitarbeiter {0} verdient {1} Euro", jim.Name, jim.Salary); jim.GetsPromotion(275); Console.WriteLine("\n Mitarbeiter {0} verdient {1} Euro", jim.Name, jim.Salary); Allerdings ist der Zugriff nicht für jeden Member des Objekts erlaubt. Die Klasse kann nämlich sensible Elemente vor dem Zugriff von außen (über die Objektvariable) schützen. Dies bringt uns zu den Konzepten und Zielen, die mit der Klassendefinition verbunden sind. Der null-Verweis Wenn Sie in einer Variable den Verweis auf ein Objekt speichern: Employee assistant = new Employee(); verweist die Variable so lange auf das Objekt, bis die Variable aufhört zu existieren oder Sie ihr einen Verweis auf ein anderes Objekt zuweisen: assistant = new Employee(); // Zuweisung eines anderen Objekts Soll die Variable zeitweilig ohne Objektverweis sein, weisen Sie ihr null zu: assistant = null; An kritischen Stellen können Sie dann prüfen, ob die Variable auf ein Objekt verweist oder nicht: if (assistant != null) { // assistant verweist auf ein Objekt, also weiter ... Zugriffe über null-Referenzen lösen zur Laufzeit eine NullReferenceException-Ausnahme aus. Statt auf null zu prüfen, können Sie fehlerhafte Zugriffe also auch durch eine Ausnahmebehandlung (siehe Kapitel 15) abfangen: try { Console.WriteLine(obj1.iStorage); } catch (Exception) { Console.WriteLine("Fehler: Zugriff über null-Verweis"); } Die Klasse als Zentrum der objektorientierten Programmierung Eine Klasse zu definieren, bedeutet weit mehr als einfach nur die Felder und Methoden aufzulisten, die dem Objekten der Klasse gemein sind. Eine gute Klassendefinition sollte Wert legen auf: Abgeschlossenheit Sichere Verwendung Kapselung Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 114 Kapitel 5: Anatomie eines C#-Programms Kapselung Die Klasse verbirgt die Interna ihrer Implementierung vor dem Benutzer. Trennung von Schnittstelle und Implementierung. Klasse Abgeschlossenheit Sicherheit Die Klasse vereint in sich funktionell zusammen gehörende Daten und Methoden. Die Klasse schützt sich selbst vor unsachgemäßem Gebrauch. (Versch. Techniken: Konstruktor, Zugriffsmodifizierer, public-Methoden vermitteln Zugriff auf private Felder.) Abbildung 5.3 Mit dem Klassen-Design verknüpfte Ideen und Konzepte Abgeschlossenheit Eine Klasse sollte in sich alle Elemente vereinen, die nötig sind, damit mit den Objekten der Klasse sinnvoll gearbeitet werden kann. So wäre z.B. eine Klasse Counter, die keine Felder zum Speichern des aktuellen Zählerstandes oder Methoden zum Weiterdrehen und Zurücksetzen des Zählers definiert, vermutlich ziemlich nutzlos. Felder wie Color oder Methoden wie ChangeColor() haben dagegen in einer Klasse Counter üblicherweise nichts zu suchen – es sei denn, der spezielle Einsatz der Klasse in einer Anwendung erfordert es. using System; class Counter { public int Value; public void Turn() { ++Value; if (Value > 999) Value = 0; } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 115 public void Reset() { Value = 0; } } class Program { static void Main(string[] args) { Counter counter = new Counter(); counter.Value = 0; counter.Turn(); Console.WriteLine(counter.Value); } } Sichere Verwendung Als Autor einer Klasse sollten Sie stets bemüht sein, die Programmierung mit der Klasse und ihren Objekten so einfach und sicher wie möglich zu gestalten. Die Klasse unterstützt dies mit Konstruktoren Zugriffsrechten Der Konstruktor wird automatisch im Zuge der Objekterzeugung aufgerufen und soll sicherstellen, dass das Objekt in einen sinnvollen Anfangszustand gesetzt wird. Er wird vor allem dazu genutzt, den Feldern sinnvolle Anfangswerte zuzuweisen oder nötige Initialisierungsarbeiten, beispielsweise die Anforderung externen Ressourcen (Dateien, Internet-Verbindungen etc.), zu erledigen. class Counter { public int Value; public Counter(int startvalue) { if (startvalue < 1000) { Value = startvalue; } else { Value = 0; } } // wie gehabt } class Program { Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 116 Kapitel 5: Anatomie eines C#-Programms static void Main(string[] args) { Counter counter = new Counter(0); counter.Turn(); Console.WriteLine(" " + counter.Value); } } Durch die Vergabe von Zugriffsrechten für Member kann die Klasse steuern, wer auf die Member zugreifen kann. Beispielsweise kann die Klasse mit dem Zugriffsspezifizierer private festlegen, dass die betroffenen Member privat sind und nur innerhalb der Klassendefinition verwendet werden können. Als public deklarierte Elemente sind dagegen öffentlich und unterliegen keinerlei Zugriffsbeschränkung (d.h. sie können über die Objekte angesprochen werden). HINWEIS Member, die ohne Zugriffsmodifizierer deklariert werden, sind in C# standardmäßig private. Die Vergabe von Zugriffsrechten ist die Grundlage zweier weiterer Konzepte: Der Vorstellung von der Klasse als Black Box, meist als »Kapselung« bezeichnet (siehe den nachfolgenden Abschnitt). Der durch Methoden moderierte Zugriff auf Felder. Greifen wir hierzu noch einmal das Beispiel der Klasse Counter auf. Die Objekte der Klasse sollen dreistellige Zähler repräsentieren, die nur in Einerschritten weitergedreht und bei Erreichen der 999 auf 0 zurückgestellt werden. Solange der Benutzer der Counter-Objekte immer brav die Methode Turn() aufruft, ist die korrekte Funktionsweise der Objekte sichergestellt. Sobald er aber auf die Idee kommt, Value direkt zu manipulieren, kann er den internen Zähler beliebig vor- oder zurückstellen und sogar Werte über 999 zuweisen. Ein allgemein übliches Konzept ist es daher, Felder durch private-Deklaration vor dem direkten Zugriff zu schützen und ihre Manipulation nur über public-Methoden und -Konstruktoren zu gestatten. In deren Implementierung kann der Autor der Klasse dann sicherstellen, dass die angestrebte Manipulation stets so ausgeführt wird, dass die Integrität des Objekts nicht verletzt wird. Wenn Sie in der Klasse Counter das Feld Value als private deklarieren, kann der Wert des Zählers nur über den Konstruktor oder eine der Methoden Turn() und Reset() gesetzt oder verändert werden. Da diese dem Feld Value nur Werte kleiner als 1000 zuweisen, ist die korrekte Verwendung der Counter-Objekte sichergestellt. Allerdings gibt es jetzt auch keine Möglichkeit mehr, den aktuellen Stand des Zählers – sprich den Wert von Value – abzufragen. Hierfür muss eine zusätzliche public-Methode definiert werden: class Counter { private int value; public Counter(int startvalue) { // wie oben } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 117 public void Turn() { // wie oben } public void Reset() { // wie oben } public int GetValue() { return value; } } class Program { static void Main(string[] args) { Counter counter = new Counter(0); counter.Turn(); Console.WriteLine(" " + counter.GetValue()); } } HINWEIS Öffentliche Methoden, die allein dazu dienen, den Wert von private-Feldern abzufragen oder zu setzen, werden in der objektorientierten Programmierung auch als Get-/Set-Methoden bezeichnet. In C#-Code findet man sie allerdings eher selten, denn für diese Aufgabe gibt es in C# einen speziellen Member-Typ: die Eigenschaft (siehe Kapitel 10). Kapselung Ein wichtiger Grundsatz objektorientierter Programmierung ist, nur die Member als public zu deklarieren, die für die Arbeit mit den Objekten nötig sind. Member, die lediglich der internen Implementierung dienen, sollten hingegen private sein. Auf diese Weise wird die Klasse zu einer Black Box, vergleichbar einem elektronischen Gerät, beispielsweise einem DVD-Player. Nach außen stellt der DVD-Player dem Benutzer alle Bedienelemente und Anschlüsse zur Verfügung, die für die Verwendung des DVD-Players benötigt werden (Benutzerschnittstelle). Was der DVD-Player sonst noch an Elementen enthält und was sich hinter den Bedienelementen tatsächlich verbirgt, versteckt der DVD-Player in seinem Gehäuse (Implementierung). Die Trennung in öffentliche Schnittstelle und private Implementierung wird in der objektorientierten Programmierung als Kapselung bezeichnet. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 118 Kapitel 5: Anatomie eines C#-Programms Konzepte, die auf der Klasse aufbauen Die Klasse ist nicht nur Gegenstand wichtiger objektorientierter Konzepte und Ideen, sie bildet gleichzeitig auch die Basis und Grundlage für Techniken wie: Modularität Vererbung Polymorphie Kapselung Die Klasse verbirgt die Interna ihrer Implementierung vor dem Benutzer. Trennung von Schnittstelle und Implementierung. Vererbung Polymorphie Klasse Abgeschlossenheit Sicherheit Die Klasse vereint in sich funktionell zusammen gehörende Daten und Methoden. Die Klasse schützt sich selbst vor unsachgemäßem Gebrauch. (Versch. Techniken: Konstruktor, Zugriffsmodifizierer, public-Methoden vermitteln Zugriff auf private Felder.) Modularität Abbildung 5.4 OOP-Konzepte, die mit der Implementierung von Klassen verbunden sind bzw. auf dem Klassenkonzept basieren Modularität Größere Software-Projekte lassen sich nur durch Aufteilung des Codes in einzelne Module bewältigen. Die Module können dann getrennt voneinander entwickelt und getestet und anschließend zum fertigen Programm zusammengesetzt zu werden. Die objektorientierte Programmierung fördert durch die Aufteilung des Codes in Klassendefinitionen automatisch die Modularisierung. Klassen sind in sich abgeschlossene Module, die gut zu warten und leicht wieder zu verwenden sind. Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 119 Objektorientierte Programmierung in C# Vererbung Klassen haben noch einen weiteren Vorzug, der ihre Wiederverwertbarkeit betrifft. Sie sind vererbbar, das heißt, eine Klasse kann von einer anderen Klasse deren Felder und Methoden übernehmen. Auf diese Weise können ganze Klassenhierarchien entstehen – etwa eine Formen-Hierarchie für ein Grafikprogramm. Am Beginn der Hierarchie könnte eine Basisklasse Shape stehen, die zwei Member definiert, über die später alle Formobjekte verfügen sollen: ein Feld rp, das einen Referenzpunkt zum Positionieren definiert, und eine Methode Move() zum Verschieben der Form. Von Shape werden dann Klassen für die einzelnen Formen abgeleitet: Line, Polygon und Circle. Diese Klassen erben die Funktionalität von Shape und definieren zusätzlich eigene Member, allen voran Felder zum Speichern der formspezifischen Daten wie z.B. Anfang- und Endpunkt für Line oder Mittelpunkt und Radius für Circle. Von den abgeleiteten Klassen können wiederum Klassen abgeleitet werden. Beispielsweise bietet es sich an, von der Klasse Polygon zwei Klassen Rectangle und Square abzuleiten, die auf diese Weise die gesamte Polygon-Funktionalität inklusive der Shape-Member erben. Shape Line Polygon Rectangle Circle Square Abbildung 5.5 Aufbau einer Klassenhierarchie Eine solche Hierarchie ist klar strukturiert und spart durch die Vererbung viel unnötigen Programmieraufwand. Umgesetzt in C#-Code würde sie etwa wie folgt aussehen: using System; // Basisklasse class Shape { // Referenzpunkt zum Verschieben und Positionieren der Form protected Point rp = new Point(); // Point sei an anderer Stelle definiert // Konstruktor public Shape(Point p) { rp = p; } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 120 Kapitel 5: Anatomie eines C#-Programms // Methode zum Verschieben, wird von allen abgeleiteten Klassen genutzt public void Move(int dx, int dy) { rp.x += dx; rp.y += dy; } } // Abgeleitete Klassen class Line : Shape { protected Point start; protected Point end; // Der Line-Konstruktor ruft über base den Konstruktor der Basisklasse auf, // um den Referenzpunkt zu setzen. public Line(Point s, Point e) : base(s) { start = s; end = e; } } class Circle : Shape { protected Point center; protected int radius; // Der Circle-Konstruktor ruft über base den Konstruktor der Basisklasse auf, // um den Referenzpunkt zu setzen. public Circle(Point p, int r) : base(p) { center = p; radius = r; } } class Polygon : Shape { protected Point[] nodes; // Der Polygon-Konstruktor ruft über base den Konstruktor der Basisklasse auf, // um den Referenzpunkt zu setzen. public Polygon(Point[] nodes) : base(nodes[0]) { this.nodes = nodes; } } class Rectangle { // Benötigt // verpackt // übergibt : Polygon keine eigenen Felder für die Form-Daten. Der Rectangle-Konstruktor einfach die Point-Objekte für die vier Eckpunkte in einem Array und dieses an den Basisklassenkonstruktor, der die Punkte im Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 121 // geerbten nodes-Feld speichert und den Referenzpunkt setzt public Rectangle(Point lo, Point lu, Point ro, Point ru) : base(new Point[4] {lo, lu, ro, ru}) { } } class Square : Polygon { // Für Erläuterung siehe den Konstruktor der Klasse Rectangle public Square(Point lo, int width) : base(new Point[4] {lo, new Point(lo.x, lo.y - width), new Point(lo.x + width, lo.y), new Point(lo.x + width, lo.y - width)}) { } } Der Code, insbesondere die Implementierung der Konstruktoren, ist schon recht anspruchsvoll und für OOP-Einsteiger sicher schwer zu verstehen. Was aber auch ohne Vorkenntnisse in objektorientierter Programmierung deutlich ins Auge fällt, ist, dass die Klasse Square außer einem Konstruktor keine eigenen Member definiert! Dies liegt natürlich daran, dass die Klasse alle benötigten Member von ihren Basisklassen Polygon und Shape erbt. Ein zugegebenermaßen extremer Fall von Code-Wiederverwertung durch Vererbung, aber nicht unbedingt außergewöhnlich. HINWEIS Konstruktoren werden nicht vererbt. Wie kann mit einem Square-Objekt programmiert werden? Der Square-Konstruktor erwartet als Argumente ein Point-Objekt, das die linke obere Ecke bezeichnet und eine Seitenlänge. Die Instanziierung eines Square-Objekts könnte demnach wie folgt aussehen: Point p = new Point(-10, 10); Square square = new Square(p, 20); Der Square-Konstruktor berechnet aus den Koordinaten der linken oberen Ecke und der Seitenlänge die Point-Objekte für alle vier Ecken und übergibt diese als Array an den Konstruktor der Basisklasse Polygon. Dieser speichert das Array mit den Point-Objekten im geerbten Feld square.nodes und reicht das erste PointObjekt an den Konstruktor von Shape weiter, der das Objekt im geerbten Feld square.rp speichert. Der Programmierer, der Square instanziiert, kann selbst allerdings nicht auf square.nodes oder square.rp zugreifen, da diese in ihren Klassen als protected deklariert sind. Der Zugriffsmodifizierer protected erlaubt den Zugriff nur von innerhalb der eigenen oder abgeleiteten Klassen aus. Allerdings gibt es auch einen öffentlichen vererbten Member: die Methode Move(). Dieses kann für jedes Objekt einer Klasse aus der Shape-Hierarchie aufgerufen werden: square.Move(-5, 20); Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) 122 Kapitel 5: Anatomie eines C#-Programms Danach speichert der Referenzpunkt des square-Objekts die Koordinaten (–15, 30). HINWEIS Klassen, die ihre Funktionalität vererben, werden als Basisklassen oder Super-Klassen bezeichnet. Klassen, die von einer bestehenden Klasse erben, nennt man abgeleitete Klassen oder Sub-Klassen. In C# gehen alle Klassen automatisch auf die oberste Basisklasse Object zurück, von der sie eine gewisse Grundfunktionalität erben. Polymorphie Abgeleitete Klassen zeichnen sich in der Regel nicht nur durch hinzukommende Merkmale und Verhaltensweisen aus. Es ist auch möglich, dass sie Verhaltensweisen (sprich Methoden) höherer Klassen erben und modifizieren. So müssen beispielsweise alle Formen aus der im vorigen Abschnitt beschriebenen Shape-Hierarchie auch gezeichnet werden. Es liegt also nahe, eine entsprechende Methode Draw() in Shape zu definieren und an die abgeleiteten Klassen zu vererben. Ein Shape-Objekt zu zeichnen, erfordert aber ganz anderen Code als das Zeichen einer Linie oder eines Kreises. Die objektorientierte Programmierung erlaubt daher in abgeleiteten Klassen das geerbte Verhalten (sprich die Methode) durch Überschreibung anzupassen. Das folgende Code-Fragment zeigt dies exemplarisch an der abgeleiteten Klasse Line: class Shape { // wie oben // Methode zum Zeichen, wird von abgeleiteten Klassen überschrieben public virtual void Draw() { Console.WriteLine(" Form ({0},{1}) ", rp.x, rp.y); } } class Line : Shape { protected Point start; protected Point end; public Line(Point s, Point e) : base(s) { start = s; end = e; } // Überschreibt die geerbte Draw()-Methode public override void Draw() { Console.WriteLine(" Linie ({0},{1}) ", rp.x, rp.y); Console.WriteLine(" ({0},{1}) bis ({2},{3}) ", start.x, start.y, end.x, end.y); } } Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8) Objektorientierte Programmierung in C# 123 Die Draw()-Methode der Shape-Hierarchie wird damit zur polymorphen Methode: sie kann für alle Objekte der Klassenhierarchie aufgerufen werden und zeichnet jedes Objekt so, wie es seinem Typ gemäß ist. Shape shape = new Shape(p1); shape.Draw(); Line line = new Line(p1, p4); line.Draw(); // ... HINWEIS Richtig interessant wird die Polymorphie allerdings erst dann, wenn Sie Objekte abgeleiteter Klassen über Basisklassenvariablen ansprechen (siehe Kapitel 12). Die Klasse als Funktionensammlung Nicht jede Klasse dient dazu, eine bestimmte Art von Objekten zu beschreiben. Manche Klassen sind nichts anderes als Sammlungen von unabhängigen Methoden, Feldern und Konstanten zu einem bestimmten Themenbereich. In solchen Fällen werden die Member der Klasse als static deklariert. Statische Member existieren nur als Elemente ihrer Klasse. Der Zugriff erfolgt daher nicht über Objekte der Klasse, sondern ausschließlich über den Namen der Klasse. Klassen, die ausschließlich statische Member enthalten, werden sogar meist so implementiert, dass eine Instanzbildung gar nicht mehr möglich ist (durch Implementierung eines privaten Konstruktors; siehe in Kapitel 10 den Abschnitt »Konstruktoren«.). Prominentes Beispiel für eine Klasse mit ausschließlich statischen Member ist System.Math. Die in Math definierten statischen Methoden implementieren häufig benötigte mathematische Funktionen wie Sinus, Kosinus, Betrag, Logarithmus und so weiter. double angle; double sinus; Console.Write(" Geben Sie einen Winkel im Bogenmaß ein: "); angle = Convert.ToDouble(Console.ReadLine()); // Berechnung des Sinus mit der statischen Math-Methode Sin() sinus = Math.Sin(angle); Dirk Louis, Shinja Strasser; Klaus Löffelmann: Microsoft Visual C# 2005 - Das Entwicklerbuch. Microsoft Press 2006 (ISBN 978-3-86063-543-8)