Programmieren in C - Hochschule Esslingen
Transcrição
Programmieren in C - Hochschule Esslingen
Programmieren in C# Prof. Dr.-Ing. Harald Melcher Hochschule Esslingen - University of Applied Sciences Dieses Skript “Programmieren in C# ” darf in seiner Gesamtheit nur zum privaten Studiengebrauch benützt werden. Das Skript ist in seiner Gesamtheit urheberrechtlich geschützt. Folglich sind Vervielfältigungen, Übersetzungen, Mikroverfilmungen, Scan-Vervielfältigungen, Verbreitung sowie die Einspeicherung und Verarbeitung in elektronischen Systemen unzulässig. Ein darüber hinausgehender Gebrauch ist zivil- und strafrechtlich unzulässig. 3 Inhaltsverzeichnis 1 Bezeichner, Typen und Zugriffsrechte 1.1 Wertetypen . . . . . . . . . . . . . 1.2 Referenztypen . . . . . . . . . . . . 1.3 Einpacken in Objekte . . . . . . . . 1.4 Variablen-Initialisierung . . . . . . . 1.5 Zugriff und Haltbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 7 7 7 8 2 Kontrollstrukturen 10 2.1 Bedingungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3 Klassen 3.1 Konstruktor . . 3.2 Destruktor . . . 3.3 Versiegelt . . . 3.4 Partielle Klassen 3.5 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 16 20 22 23 23 4 Methoden und Eigenschaften 4.1 Methoden-Parameter . . . . . . . . . 4.2 Methoden mit variabler Parameterzahl 4.3 Überschreiben von Methoden . . . . . 4.4 Operator-Überschreiben . . . . . . . . 4.5 Eigenschaften (Properties) . . . . . . 4.6 Indexer . . . . . . . . . . . . . . . . . 4.7 Signatur . . . . . . . . . . . . . . . . 4.8 Generische Methoden . . . . . . . . . 4.9 die Main-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 28 29 31 31 32 34 35 35 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Interfaces 5.1 IEnumerable . . 5.2 IEnumerator . . 5.3 IConvertible . . 5.4 IComparable . . 5.5 ICloneable . . . 5.6 IFormatable . . 5.7 IEnumerable<T> 5.8 IList<T> . . . . 5.9 ICollection<T> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 37 37 38 38 38 38 39 40 40 6 Strings 6.1 Escape-Character 6.2 Interfaces . . . . 6.3 String-Erzeugung 6.4 String-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 42 42 43 Inhaltsverzeichnis 4 6.5 6.6 6.7 Format-Angaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effiziente Stringbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Collections 7.1 List<T> . . . . . . . . . . . . . . 7.2 LinkedList<T> . . . . . . . . . . 7.3 Queue<T> . . . . . . . . . . . . . 7.4 Stack<T> . . . . . . . . . . . . . 7.5 Dictionary<TKey,TValue> . . . 7.6 SortedList . . . . . . . . . . . . 7.7 foreach über eigene Sammlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 50 53 55 55 58 60 62 63 63 63 8 Delegates, Events und Exceptions 65 8.1 Delegates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 8.2 Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 8.3 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 9 Threads 74 9.1 Threads erzeugen und verwalten . . . . . . . . . . . . . . . . . . . . . . . . . . 74 9.2 Prioritäten von Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 9.3 Synchronisation von Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 10 Dateien 79 10.1 Die Laufwerke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 10.2 Verzeichnisse und Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 10.3 Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 11 Forms und Controls 11.1 Label . . . . . . . . . . . . . . . . . . . . 11.2 MessageBox . . . . . . . . . . . . . . . . . 11.3 Button . . . . . . . . . . . . . . . . . . . . 11.4 CheckBox . . . . . . . . . . . . . . . . . . 11.5 RadioButton . . . . . . . . . . . . . . . . 11.6 ComboBox, ListBox und CheckedListBox 11.7 ListView . . . . . . . . . . . . . . . . . . 11.8 TextBox . . . . . . . . . . . . . . . . . . . 11.9 RichTextBox . . . . . . . . . . . . . . . . 11.10ProgressBar . . . . . . . . . . . . . . . . 11.11StatusStrip . . . . . . . . . . . . . . . . 11.12ContextMenuStrip . . . . . . . . . . . . . 11.13OpenFileDialog . . . . . . . . . . . . . . 11.14Eigenständige Forms . . . . . . . . . . . . . 11.15Panels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 85 85 86 87 88 89 91 93 98 99 99 100 100 101 102 12 Tipps und Tricks 12.1 Dokumentierende Kommentare 12.2 Laufzeitmessung . . . . . . . . 12.3 Zufallszahlen . . . . . . . . . . 12.4 Besondere Verzeichnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 103 106 106 106 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1 Bezeichner, Typen und Zugriffsrechte C# unterscheidet zwischen Wertetypen (value types) und Referenztypen (reference types). Das Laufzeitsystem legt Wertetypen auf dem Stack ab, was sehr schnell geht. Referenztypen legt es auf dem Heap ab und der Stack enthält nur die Referenz auf das Objekt. Das braucht mehr Zeit, weil das Laufzeitsystem erst den Platz auf dem Heap aussuchen und zuweisen muss. Wertetypen innerhalb von Referenztypen, also z.B. Werte in Objekten, liegen innerhalb des Objektes auf dem Heap. 1.1 Wertetypen C# hat eine Reihe von Wertetypen, die aus andern Programmiersprachen wie C++ oder Java bekannt sind. Es gibt vorzeichenbehaftete und vorzeichenlose Varianten von byte, int und long, sowie Gleitkommazahlen einfacher (float) und doppelter (double) Genauigkeit und einen eigenen Zahlentyp für Dezimalzahlen (decimal). Weil C# mit anderen Programmiersprachen zusammenarbeiten soll, die der .NET Common Language Specification folgen, sind für C# viel genauere Angaben zu Wertebereichen der eingebauten Typen nötig, als sie bei C oder C++ nötig waren. So ist in C++ der long-Typ meist 4 Byte lang, in C# ist er immer 8 Byte lang. Auch gibt es keine Abbildung vom Typ bool auf Integerwerte mehr – boolsche Variable sind in C# ein eigenständiger Datentyp. Speziell lassen sie sich nicht, wie in C++, mit Integertypen mischen. Das vermeidet aber netterweise einen der häufigsten Programmierfehler von C oder C++, das Verwechseln von = mit ==: bool ist nicht int if ( i = 5) printf ( " i ist fuenf " ) ; Spezielle Wertetypen sind struct und enum. Bezeichnung sbyte byte short ushort int uint long ulong float double decimal char bool enum struct Anzahl Bytes 1 1 2 2 4 4 8 8 4 8 12 2 1 ? ? Beschreibung kleine Zahl mit Vorzeichen kleine Zahl ohne Vorzeichen kurze Zahl mit Vorzeichen kurze Zahl ohne Vorzeichen normale Zahl mit Vorzeichen normale Zahl ohne Vorzeichen lange Zahl mit Vorzeichen lange Zahl ohne Vorzeichen Gleitkommazahl doppelt genaue Gleitkommazahl Dezimalzahl Zeichen in Unicode Wahrheitswert Aufzählung Zusammengesetzter Typ Tabelle 1.1: Wertetypen Wertebereich −128 . . . + 127 0 . . . 255 −32.768 . . . + 32.767 0 . . . + 65.536 −231 . . . + 231 − 1 0 . . . 232 63 −2 . . . + 263 − 1 0 . . . 264 bis zu 28 Stellen Textzeichen true oder false 1 Bezeichner, Typen und Zugriffsrechte 6 1.1.1 struct Structs sind Wertetypen Ein Struct in C# ist ein benutzerdefinierter Wertetyp. Es unterstützt Properties, Methoden, Fields und Operatoren, hingegen keine Vererbung oder Destruktoren. Durch die Ablage auf dem Stack (als Wertetyp) kann das Laufzeitsystem Structs schnell erzeugen und auf sie zugreifen. Bei der Verwendung in untypisierten Collections sind Structs allerdings langsamer als Klassen mit gleichen Eigenschaften, weil das Laufzeitsystem sie erst in Objekte einpacken (boxen) muss. 1.1.2 enum Enum ist ein benutzerdefinierter Aufzählungstyp. Intern verwendet C# zur Darstellung einen Integertyp (byte, int oder long), je nach Anzahl der angebenen Elemente. Standardtyp ist int. using System ; class Program { public enum getraenk : byte { Bier , // Automatisch 0 Wein , // Automatisch 1 Milch , // Automatisch 2 Met = 66 , // Erzwungen 66 Absynth // Automatisch 67 } static void Main ( string [] args ) { getraenk g = getraenk . Bier ; Console . WriteLine ( g ) ; } } Die abstrakte Basisklasse von Enum ist System.Enum, so dass C# eine Reihe von Methoden bereitstellen kann, falls das Programm einen Enum-Wert wie ein Objekt anspricht. Speziell interessant sind die Funktionen GetNames() und GetValues(), die Arrays mit den Namen oder Werten eines Enum-Typs zurückgeben. using System ; class Program { public enum getraenk : byte { Bier , // Automatisch 0 Wein , // Automatisch 1 Milch , // Automatisch 2 Met = 66 , // Erzwungen 66 Absynth // Automatisch 67 } static void Main ( string [] args ) { string [] gs = Enum . GetNames ( typeof ( getraenk ) ) ; foreach ( String s in gs ) Console . WriteLine ( s ) ; }} 1.2 Referenztypen 7 1.1.3 var Var ist eine implizit definierte lokale Variable: Der C# -Compiler erkennt den Typ der Variablen aus ihrem Initialwert. Daher braucht eine var-Variable immer gleich bei der Deklaration eine Wertzuweisung. Dies ist vor allem bei den später vorkommenden anonymen Klassen praktisch. var var var var index = 5; fach = " Programmieren in C # " ; schiffe = new List < Schiff >; person = new { Vorname = " Martin " , Nachname = " Maier " , MatrNr =732388}; 1.2 Referenztypen Alle Klassen sind Referenztypen. Das Laufzeitsystem legt sie als Objekt auf dem Heap an und die dazugehörige Referenz, die auf das Objekt zeigt, auf dem Stack. Bei Methodenaufrufen übergibt das Laufzeitsystem die Referenz auf das Objekt als Kopie – das Objekt selber bleibt nur einfach auf dem Heap. Das bedeutet, dass Manipulationen am Objekt innerhalb einer Methode den Inhalt des Original-Objekts verändern. Falls das unerwünscht ist, liefert der Copy-Konstruktor eine Kopie des Objekts mit einstellbarer Tiefe. 1.3 Einpacken in Objekte Jeder Typ in C# leitet sich von Object ab und so wandelt das Laufzeitsystem Wertetypen automatisch in Objekte um, wenn das Programm deren Objekteigenschaften anspricht. Diesen Vorgang des Einpackens nennt man boxing. Boxing erzeugt ein Objekt auf dem Heap, das den Wertetyp zum Inhalt hat und legt eine Referenz darauf auf den Stack. Das folgende Programm packt den Wert von i automatisch in ein Objekt ein, um dessen ToString()-Methode aufrufen zu können. class Program { static void Main ( string [] args ) { String s = " Ein String " ; int i = 42; System . Console . WriteLine ( s . ToString () ) ; System . Console . WriteLine ( i . ToString () ) ; } } Dieser Vorgang kostet Zeit und bringt daher Geschwindigkeitsverluste zur Laufzeit. Um eine schnelle Programmausführung zu gewährleisten, sollte die Programmiererin darauf achten, dass das Laufzeitsystem nicht Wertetypen einpacken muss, um sie z.B. in Collections unterzubringen. Das Auspacken eines Werts aus einem Objekt erfolgt nicht automatisch, sondern das Programm muss gezielt nachsehen, ob das Objekt den richtigen Typ eingepackt hat und den Wert aus dem Objekt dann in eine Werte-Variable kopieren. Falls das Objekt null ist oder den falschen Wertetyp kapselt, dann wirft das Laufzeitsystem eine Exception, die invalidCastException. 1.4 Variablen-Initialisierung Alle Variablen, die C# verwendet, müssen bei der ersten lesenden Verwendung einen zugewiesenen Wert haben. Die Deklararation einer Variablen muss ihr noch keinen Wert zuweisen, aber zur 1 Bezeichner, Typen und Zugriffsrechte 8 ersten Verwendung muss die Variable einen Wert haben, das stellt der Compiler sicher: Falls die Programmiererin Variable innerhalb einer Klasse oder eines Structs nicht bei der Deklaration explizit initialisiert hat, setzt der Compiler sie auf Null. Variable, die lokal in einer Methode sind, muss die Programmiererin vor Gebrauch durch das Programm initialisieren, entweder durch eine explizite Zuweisung bei der Deklaration oder durch eine Zuweisung zur Laufzeit. Dazu prüft der Compiler alle möglichen Codewege ab und meldet als Fehler, wenn er einen Weg findet, auf dem die Variable nicht initialisiert wird. Variablen-Initialisierung erfolgt entweder bei der Deklaration einer Klasse für die einzelnen Variablen oder innerhalb eines Konstruktors. Falls Variable im Programm als Referenz-Parameter einer Methode gedacht sind, die einen Wert zurückliefern, so gibt es dazu in C# den out - Parametertyp einer Methode. Der C# Compiler weiß dann, dass er im aufrufenden Programmteil nicht auf die Initialisierung der Variablen achten muss, da sie innerhalb der Methode als “ohne Wert” bekannt ist. int a ; static int zwei = 2; static double r = 4.5; double umfang = zwei * Math . PI * r ; double flaeche = Math . PI * Math . Pow (r , 2.0) ; 1.5 Zugriff und Haltbarkeit Variable und Methoden ohne weitere Angabe sind privat 1.5.1 Privat Alle Variablen und Methoden in C# sind, wenn nicht anders angegeben, privat. Dann können nur Methoden aus der Klasse, in der eine Variable definiert ist, auf die Variable zugreifen und nur Methoden von innnerhalb der Klasse deren private Methoden aufrufen. Zugriff auf die Variablen kann dennoch erfolgen über Properties. Dies erlaubt kontrollierteren Zugriff auf Variable und dient dem Information-Hiding, das heißt dem Verstecken der Vorgänge im Innern. So lässt die Trennung zwischen Zugriff auf Variable und deren interner Repräsentation bei der Programmierung Platz für Flexibilität. Private Variable und Methoden können zur Klarstellung den Zugriffs-Modifikator private tragen. 1.5.2 Öffentlich Alle Methoden und Variable, die von außerhalb sichtbar und zugreifbar sein sollen, sind public. Nur die wirklich nach außen benötigten Methoden öffentlich zu machen, ist eine Methode der Verringerung der Komplexität und lässt der Programmiererin mehr Freiheiten bei der ImplemenVon Inter- tation der Klasse. Speziell Methoden, die von einem Interface herrühren, müssen öffentlich sein, faces geerbte denn die Implementierung eines Interfaces ist ein “Versprechen” der implementierenden Klasse, Methoddie Methoden des Interfaces zu anzubieten. en müssen öffentlich sein 1.5.3 Geschützt Auf protected deklarierte Variable und Methoden können Methoden der eigenen Klasse und die davor abgeleiteten Klassen zugreifen. protected erweitert damit private dahin, dass auch Methoden der abgeleiteten Klassen auf die Variablen oder Methoden zugreifen dürfen. 1.5 Zugriff und Haltbarkeit 9 1.5.4 Intern Für Java- oder C++-Programmiererinnen neu ist der Zugriffsmodifikator internal. Bei internal dürfen andere Klassen auf die internal deklarierten Variablen und Methoden zugreifen als wären sie public. Aber nur, solange die andern Klassen im gleichen Assembly liegen, wie die Klasse, auf die sie zugreifen. 1.5.5 Statisch Statische Variable sind einmal pro Klasse vorhanden – alle Instanzen teilen sich ein und dieselbe Variable. Das spart zum einen Speicherplatz, zum andern erlauben sie aber auch die Kommunikation zwischen verschiedenen Instanzen einer Klasse. Eine typische Anwendung ist das Mitzählen, wieviel Instanzen einer Klasse es gerade gibt. class Program { static int instanceCounter ; public Program () { instanceCounter ++; } static void Main ( string [] args ) { Program p ; p = new Program () ; p = new Program () ; Console . WriteLine ( instanceCounter ) ; } } Näheres zum statischen Konstuktor findet sich unter 3.1.2 auf Seite 18 10 2 Kontrollstrukturen 2.1 Bedingungen 2.1.1 If Wie Java oder C++ kennt C# auch das if-Statement und kennzeichnet den alternativen Ausführungspfad mit else: using System ; class Program { static void Main ( string [] args ) { int i = 4; if ( i > 3) Console . WriteLine ( " i ist größer als 3 " ) ; else Console . WriteLine ( " i ist kleiner / gleich 3 " ) ; } } Mehrere Anweisungen als Folge einer Bedingung fasst C# mit geschweiften Klammern in einen Block zusammen: using System ; class Program { static void Main ( string [] args ) { int i = 4; if ( i > 3) { Console . WriteLine ( " i ist größer als 3 " ) ; i -= 1; } else { Console . WriteLine ( " i ist kleiner / gleich 3 " ) ; i += 1; } } } Anders als C lässt C# innerhalb der Bedingung einer if-Anweisung nur einen boolschen Wert zu. Das schließt einen der häufigsten C-Programmierfehler aus: if ( i = 3) printf ( " i ist 3 " ) ; 2.1 Bedingungen 11 2.1.2 Switch Das switch-Statement eignet sich besonders gut für die Abfrage einer Reihe sich gegenseitig ausschließender Bedingungen. Das Argument der switch-Anweisung gibt an, welchen Ausdruck die darauf folgenden case-Anweisungen prüfen sollen. Ein abschließendes default-Statement fängt alle Fälle ein, die die case-Anweisungen nicht abdecken. Anders als in C++ braucht jeder Jede casecase-Ast eines switch-Statements ein abschließendes break! Bedingung bei switch using System ; braucht ein break class Program { static void Main ( string [] args ) { int i ; i = Convert . ToInt16 ( Console . ReadLine () ) ; switch ( i ) { case 0: Console . WriteLine ( " Nix " ) ; break ; case 1: Console . WriteLine ( " Eins " ) ; break ; case 2: Console . WriteLine ( " Zwei " ) ; break ; case 3: Console . WriteLine ( " Drei " ) ; break ; default : Console . WriteLine ( " Viele " ) ; break ; } } } Nur wenn ein Ast eines Switch-Statements keinen Code erzeugt, dann ist Durchfallen zum nächsten Ast möglich, um völlig gleichartige Fälle zusammenzufassen: switch ( i ) { case 0: Console . WriteLine ( " Nix " ) ; break ; case 1: case 2: case 3: Console . WriteLine ( " Wenige " ) ; break ; default : Console . WriteLine ( " Viele " ) ; break ; } Das“Springen”zu einem andern Ast eines switch-Statements bewirkt in C# die goto-Anweisung, die jedoch nicht sonderlich zur Lesbarkeit des Codes beiträgt: using System ; class Program { static void Main ( string [] args ) { string f = Console . ReadLine () ; 2 Kontrollstrukturen 12 switch ( f ) { case " Fahrrad " : Console . WriteLine ( " Ein Rad " ) ; goto case " Einrad " ; case " Einrad " : Console . WriteLine ( " ... und noch ein Rad " ) ; break ; case " Auto " : Console . WriteLine ( " Drei Räder " ) ; goto case " Einrad " ; default : Console . WriteLine ( " Unbekanntes Fahrzeug : " + f ) ; break ; } } } 2.2 Schleifen 2.2.1 for Die for-Schleife bietet die Möglichkeit, einen Block mehrfach zu durchlaufen und vor jedem Durchlauf zu prüfen, ob eine bestimmte Bedingung noch erfüllt ist. Sie hat folgenden Aufbau: for ( Initialisierung ; Abbruch - Bedingung ; Iterator } Statement ( s ) mit Initialisierung Diesen Code führt das Laufzeitsystem aus, bevor es das erste Mal die Schleife durchläuft. Oft initialisiert dieser Code die Schleifenvariable, wobei C# hier die Deklaration einer Schleifenvariablen zulässt. Abbruch-Bedingung Das Erfüllen dieser Bedingung entscheidet über einen (weiteren) Durchlauf der Schleife. Falls die Bedingung zu Anfang nicht erfüllt ist, findet überhaupt kein Schleifendurchlauf statt. Die Bedingung muss einen booleschen Wert ergeben. Iterator Das Laufzeitsystem führt den Iterator nach jedem Schleifendurchlauf aus. Das Ergebnis geht nicht in die Entscheidung über einen weiteren Schleifendurchlauf ein. Jedoch findet direkt nach Ausführen des Iterators das Abprüfen der Bedingung statt, so dass sie Entscheidung direkt nach dem Iterator fällt. Oft besteht der Iterator aus dem Hoch- oder Runterzählen des Schleifenzählers. Das folgende Programm 1. deklariert die Schleifenvariable i im Initialisierer 2. prüft dann die Abbruch-Bedingung 3. führt die Schleife aus 4. führt den Iterator aus 5. macht beim Prüfen der Abbruch-Bedingung (Punkt 2) weiter 13 2.2 Schleifen using System ; class Program { static void Main ( string [] args ) { for ( int i = 1; i < 100000; i *= 2) Console . WriteLine ( i ) ; } } Recht häufig tregen mehrfach verschachtelte Schleifen auf. Typischerweise treten doppelt verschachtelte bei 2-dimensionalen Aufgabenstellungen und dreifach verschachtelte bei 3dimensionalen auf. Die “innere” Schleife läuft dabei am schnellsten durch und die äußeren immer langsamer, je weiter außen sie liegen. Das automatische Einrücken des Text-Editors hilft dabei, die Übersicht zu behalten: using System ; class Program { static void Main ( string [] args ) { int x , y ; for ( y = 1; y <= 10; y ++) { for ( x = 1; x <= 10; x ++) Console . Write ( " {0 ,3} " , x * y ) ; Console . WriteLine () ; } } } ergibt die Tabelle mit dem kleinen 1x1: 1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 10 18 20 27 30 36 40 45 50 54 60 63 70 72 80 81 90 90 100 2.2.2 while Die while-Schleife von C# funktioniert genau so wie die in C++ oder Java: Das Laufzeitsystem führt ein Statement oder einen Block so lange aus, wie die Schleifenbedingung erfüllt ist. Der Test auf Erfüllung der Schleifenbedingung erfolgt vor jedem Schleifendurchlauf, also auch vor dem ersten. Bei Nichterfüllen der Bedingung führt das Laufzeitsystem das Statement oder den Block überhaupt nicht aus. using System ; class Program { 2 Kontrollstrukturen 14 static void Main ( string [] args ) { int f1 =1 , f2 =1; while ( f2 < 100000) { int nextGen = f1 + f2 ; f1 = f2 ; f2 = nextGen ; Console . WriteLine ( nextGen ) ; } } } Dieses Programm erzeugt eine uns wohlbekannte Zahlenfolge: 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 2.2.3 do...while Die do...while-Schleife ist eine Variante der while-Schleife, nur mit dem Test nach dem Anweisungsteil. Das bedeutet auch, dass das Laufzeitsystem den Anweisungsteil der do...whileSchleife mindestens einmal zu Beginn ausführt, selbst wenn schon zu Schleifenbeginn die Bedingung nicht erfüllt ist. using System ; class Program { static void Main ( string [] args ) { double zahl =2 , f1 =1 , f2 , diff ; do { // Heron - Verfahren 15 2.2 Schleifen f2 = ( f1 + zahl / f1 ) / 2; Console . WriteLine ( f2 ) ; if ( f2 > f1 ) diff = f2 - f1 ; else diff = f1 - f2 ; f1 = f2 ; } while ( diff > 0.000000001) ; } } Dieses Programm nähert eine häufig verwendete Zahl an: 1 ,5 1 ,41666666666667 1 ,41421568627451 1 ,41421356237469 1 ,41421356237309 2.2.4 foreach Zusätzlich zu den normalen Schleifen-Konstruktionen wie for und while besitzt C# auch noch die foreach-Schleife, die durch alle Elemente einer Sammlung (Collection) iteriert. Die Sammlung besteht aus einzelnen Elementen und die foreach-Schleife durchläuft alle Elemente in einer Reihenfolge, die die Programmiererin der Sammlung festgelegt hat. foreach ( ElementTyp element in Sammlung } Statement ( s ) Der Wert des einzelnen Elements lässt sich dabei innerhalb der Statement-Blocks nicht ändern. using System ; class Program { static void Main ( string [] args ) { String [] getraenke = { " Bier " , " Milch " , " Wasser " }; foreach ( String g in getraenke ) Console . WriteLine ( g ) ; } } 2.2.5 break Das break-Statement kam bereits beim switch-Statement vor, wo es die Abarbeitung eines Verzweigungs-Astes beendet. Bei Schleifen beendet break die Ausführung einer Schleife sofort und das Programm macht am ersten Statement nach der Schleife weiter. Das gilt für alle Schleifen, also für for, while, do...while und foreach. 2.2.6 continue Das continue-Statement beendet die Ausführung des aktuellen Schleifendurchlaufs sofort und leitet den nächsten Schleifendurchlauf ein. Es tritt bei for, while, do...while und foreach auf. 16 3 Klassen Jede Klasse in C# erbt von der Object-Klasse und stellt damit auch deren öffentliche Methoden Equals, GetHashCode, GetType, ReferenceEquals und ToString zur Verfügung. Gerade ToString ist sehr nützlich, weil es durch Aufruf dieser Methode zu jedem Wert in C# eine lesbare Darstellung gibt. Beim Aufruf der ToString-Methode von Wertetypen boxt C# den Wertetyp, um die ToString-Methode des kapselnden Objekts aufrufen zu können. Über Type GetType() kann ein Programm zur Laufzeit eine Instanz von System.Type erhalten, die weitere Informationen über das Objekt liefert (Klassenhierarchie, Basistyp, Methoden, Properties, etc.). Methode bool Equals(object o) bool Equals(object o, object p) void Finalize() int GetHashCode() Type GetType() object MemberwiseClone() bool ReferenceEquals(object o, object p) string ToString() Zugriff public virtual public static protected virtual public virtual public protected Aufgabe Testet 2 Objekte auf Gleichheit Testet 2 Objekte auf Gleichheit Die .NET-Version eines Destruktors Liefert einen objekt-eindeutigen Wert Liefert eine Instanz von System.Type Erstellt eine flache Kopies public static 2 Referenzen auf das selbe Objekt? public virtual die String-Darstellung des Objekts Tabelle 3.1: Methoden von Object 3.1 Konstruktor Konstruktoren sehen in C# aus wie in C++ oder Java. Der Konstruktor einer Klasse ist eine Methode der Klasse, die den gleichen Namen hat wie die Klasse und keinen Rückgabewert. Wie in C++ oder Java auch, braucht die Programmiererin keinen Konstruktor zu deklarieren. In dem Fall erzeugt der Compiler einen argumentenlosen Default-Konstruktor. benötigt weniger Konstruktoren als die anderen Programmiersprachen, weil der Compiler Klassenvariable zu Null initialisiert. Die Programmiererin kann Konstruktoren überladen, wie jede andere Klassenmethode auch. Wenn jedoch ein Konstruktor mit Argumenten existiert, dann erzeugt der Compiler nicht mehr automatisch eine vorzeichenlose Version davon. using System ; class Program { static void Main ( string [] args ) { DemoClass d1 = new DemoClass () ; DemoClass d2 = new DemoClass (17 , 4) ; 3.1 Konstruktor 17 } } public class DemoClass { public DemoClass () { Console . WriteLine ( " Konstruktor ohne Argumente " ) ; } public DemoClass ( int x , int y ) { Console . WriteLine ( " Konstruktor mit 2 Argumenten " ) ; } } 3.1.1 Objekt-Initialisierer Neu in C# ist der Objekt-Initialisierer, der öffentliche Objekt-Variablen vorbesetzen kann, ohne dass dafür ein Konstruktor vorhanden sein muss: using System ; class MainClass { public static void Main ( string [] args ) { Student s = new Student { Vorname = " Martin " , Nachname = " Maier " , MatrNummer = 7325633 }; } } public class Student { public String Vorname ; public String Nachname ; public int MatrNummer ; } } Nachteilig an dieser Variante ist, dass damit von andern Klassen aus Zugriff auf Member-Variable möglich ist, was der losen Kopplung der objektorientierten Programmierung widerspricht, da andere Klasse über das interne Funktionieren der aktuellen Klasse Bescheid wissen (müssen). Richtig Sinn macht der Objekt-Initialisierer daher erst beim Zugriff über öffentliche Eigenschaften. Diese können dann gleich beim Erzeugen des Objekts eine Plausibilitätsprüfung vornehmen: using System ; class MainClass { public static void Main ( string [] args ) { Student s = new Student { 3 Klassen 18 Vorname = " Martin " , Nachname = " Maier " , MatrNummer = 7325633 }; Console . WriteLine ( s . ToString () ) ; } } public class Student { private String vorname ; private String nachname ; private int matrNummer ; public String Vorname { get { return vorname ; } set { vorname = value ; } } public String Nachname { get { return nachname ; } set { nachname = value ; } } public int MatrNummer { get { return matrNummer ; } set { if ( matrNummer > 0) matrNummer = value ; } } public override string ToString () { return string . Format ( " [ Student : Vorname ={0} , Nachname ={1} , MatrNummer ={2}] " , Vorname , Nachname , MatrNummer ) ; } } 3.1.2 statischer Konstruktor Das Laufzeitsystem führt den statischen Konstruktor einer Klasse genau einmal pro verwendeter Klasse aus, bevor es anderen Code aus der Klasse ausführt. Der Code im folgenden Beispiel using System ; class Program { static void Main ( string [] args ) { DemoClass d ; d = new DemoClass () ; d = new DemoClass () ; d = new DemoClass () ; } } class DemoClass { 3.1 Konstruktor 19 static int instanceCounter = 0; static DemoClass () { Console . WriteLine ( " Statischer Konstruktor " ) ; } public DemoClass () { ++ instanceCounter ; Console . WriteLine ( " {0}: Default Konstruktor " , instanceCounter ) ; } ~ DemoClass () { Console . WriteLine ( " {0}: Destructor " , instanceCounter ) ; -- instanceCounter ; } } ergibt folgende Ausgabe: Statischer Konstruktor 1: Default Konstruktor 2: Default Konstruktor 3: Default Konstruktor 3: Destructor 2: Destructor 1: Destructor 3.1.3 Copy-Konstruktor Im Gegensatz zur“flachen”Wertezuweisung bei Objektreferenzen, die nur die Referenz verdoppeln und das Objekt einfach im Speicher liegen lassen, kann der Copy-Konstruktor eine “intelligente” Kopie einstellbarer Tiefe vornehmen. using System ; class Program { static void Main ( string [] args ) { DemoClass d , e , f ; d = new DemoClass ( " Eins " ) ; e = d; f = new DemoClass ( d ) ; d . Print ( " d " ) ; e . Print ( " e " ) ; f . Print ( " f " ) ; d . SetLabel ( " Zwei " ) ; d . Print ( " d " ) ; e . Print ( " e " ) ; f . Print ( " f " ) ; } } class DemoClass 3 Klassen 20 { String label ; public DemoClass ( String label ) { this . label = label ; } public DemoClass ( DemoClass d ) { this . label = d . label ; } public void SetLabel ( String newLabel ) { this . label = newLabel ; } public void Print ( String what ) { Console . WriteLine ( " {0}: {1} " , what , label ) ; } } Die Ausgabe zeigt den deutlichen Unterschied zwischen der reinen Zuweisung und der Verwendung des Copy-Konstruktors: d: e: f: d: e: f: Eins Eins Eins Zwei Zwei Eins 3.2 Destruktor Das Laufzeitsystem ruft den Destruktur eines Objekts auf, wenn es das Objekt entsorgen will. Das ist normalerweise nicht gleich der Fall, wenn das Objekt außer Sichtweite des Programms geht, sondern wenn der Speicher knapp wird und das Laufzeitsystem den Garbage Collector aufruft um nicht mehr referenzierte Objekte zu löschen. 3.2.1 Müllabfuhr Einer der größten Unterschiede zu C++ ist die Art, wie das C# -Laufzeitsytem mit Klassen umgeht, die außer Scope, d.h. außer Sichtbarkeit geraten sind. Die C++-Programmiererin entlastet das C# -Laufzeitsystem von den Sorgen über Speicherlecks und nicht freigegebenen Zeigern. Dafür hat sie aber nun auch keine explizite Kontrolle darüber, wann genau Objekte zerstört werden. Es gibt in C# nämlich keine expliziten Destruktor. Wenn ein Objekt nun aber eine ungemanagte Ressource beinhaltet, muss es eine Methode geben, die Ressource freizugeben, wenn das Objekt sie nicht mehr verwendet, damit andere User oder Prozesse darauf zugreifen können. Eine implizite Verwaltung der Ressource ist in der FinalizeMethode einer Klasse möglich, die der Müllsammler aufruft, wenn er das Objekt zerstört. Diese Methode heißt auch Finalizer. Ein selbstgeschriebener Finalizer soll nur ungemanagte Ressourcen freigeben, um die andern Ressourcen kümmert sich das Laufzeitsystem. Wenn die Klasse keine ungemanagten Ressourcen einsetzt, braucht die Programmiererin überhaupt keinen eigenen Finalizer zu schreiben. Ein Programm darf auch nie den Finalizer direkt aufrufen, sondern überlässt 3.2 Destruktor 21 diesen Aufruf dem Müllsammler. Innerhalb einer eventuell doch selbstgeschriebenen FinalizerMethode ist der Aufruf der Finalizer-Methode der Basisklasse jedoch erforderlich. Identisch zum Schreiben einer Finalize-Methode ist die Verwendung des C# -Destruktors. Damit sind folgende Code-Stücke äquivalent: MyClass . Finalize () { // ... // Freigeben der unmanaged Ressources hier // ... // Aufrufen des Finalizers der Basis - Klasse base . Finalize () ; } ist äquivalent zu: ~ MyClass () { // ... // Freigeben der unmanaged Ressources hier // ... // Aufruf des Basisklassen - Finalizer erledigt das Laufzeitsystem } Falls das Programm benötigte (meist externe) Ressourcen schon zur Laufzeit und ohne Warten auf den Müllsammler freigeben soll, muss die Klasse, die die Ressourcen hortet, das IDisposableInterface implementieren. Damit verspricht sie, die Methode Dispose() anzubieten, die die Aufräumarbeiten selbst erledigt. Das Programm muss dann die Methode Dispose() explizit aufrufen, um die Ressourcen freigeben zu lassen, ohne auf den Müllsammler warten zu müssen. Hat die Programmiererin nun eine Dispose()-Methode eingebaut und aufgerufen, so muss sie das Laufzeitsystem dazu bringen, beim Entsorgen des Objekts über den Müllsammler nicht noch einmal das Objekt zu entsorgen und die eingebaute Dispose()-Methode zu verwenden. Dies erreicht sie durch Aufruf der Methode GC.SuppressFinalize beim Abarbeiten der Dispose()Methode und übergibt dabei eine Referenz auf das Objekt, das keine zweite Entsorgung benötigt. Tritt der Müllsammler in Aktion und das Objekt ist aus dem Blickfeld geraten, ohne dass es das Programm mit Dispose() explizit zerstört hat, so ruft der Müllsammler die Finalize-Methode auf, deren Implementation sich praktischerweise wieder der Dispose()-Methode bedient. So rufen beide, entweder das Programm explizit oder der Müllsammler implizit, die Methode Dispose() auf und geben damit die Ressourcen des Objekts frei. public void Dispose () { // ... // Ressourcen freigeben // GC . SupressFinalize ( this ) ; } public override void Finalize () { Dispose () ; // Aufruf unserer Dipsose - Methode base . Finalize () ; } Bei einigen Klassen, z.B. zur Dateibehandlung oder bei Datenströmen, ist der Aufruf einer Close()-Methode verständlicher als der einer Dispose()-Methode. In solchen Fällen erstellt 3 Klassen 22 die Programmierierin eine private Dispose()-Methode und bietet nach außen eine öffentliche Close()-Methode an, die intern Dispose aufruft. Die Verantwortung für den Aufruf von Dispose() zum frühest möglichen Zeitpunkt liegt nun beim Programm. Vergisst das Programm, Dispose() aufzurufen, gibt erst der Finalizer beim Müllsammeln die Ressourcen frei. Als Erleichterung dafür kennt C# die using-Konstruktion. Sie stellt sicher, dass das Laufzeitsystem Dispose() zum frühest möglichen Zeitpunkt aufruft. Die Idee dahinter ist, dem Laufzeitsystem eine Reihe von Objekten der gleichen Klasse mitzuteilen und den Programmabschnitt, in dem sie gültig sind. Die Klasse der Objekte im using-Statement muss dazu das Interface IDisposable unterstützen. Sobald das Laufzeitsystem diesen Programmabschnitt verlässt, gibt sie die Objekte mit Dispose() frei. Das using-Statement schützt auch vor unerwarteten Ausnahmen. Egal, was innerhalb der using-Klammern passiert, Dispose wird auf jeden Fall aufgerufen. Speziell ist das beim Auftreten von Exceptions interessant, die dadurch möglicherweise keinen try-catch-Block brauchen, weil Dispose() auf jeden Fall aufgerufen wird. using System ; using System . IO ; class Program { static void Main ( string [] args ) { string path = @ " c :\ tmp \ logfile . txt " ; // Logfile Öffnen oder neu Erstellen using ( StreamWriter sw = File . AppendText ( path ) ) { sw . WriteLine ( " Neuer Eintrag " ) ; } // Logfile ist freigegeben // ... // Normales Programm // ... // Logfile Lesen using ( StreamReader sr = File . OpenText ( path ) ) { string s = " " ; // Für alle Fälle while (( s = sr . ReadLine () ) != null ) { Console . WriteLine ( s ) ; } } } } 3.3 Versiegelt Klassen, von denen andere Programmierer nicht ableiten sollen, kennzeichnet die Programmiererin als sealed. Der Grund, keine weitere Vererbung zuzulassen, ist meist, dass die Klasse in einer Klassenbibliothek eine interne Hilfsklasse ist, deren unveränderte Funktion wichtig für das Funktionieren der Bibliotheksklasse ist. Geade bei lizensieren Bibliotheken ist ein weiterer 23 3.4 Partielle Klassen Grund der, dass die liefernde Firma Modifikationen der Bibliothek verhindern möchte, die der Kunde nicht lizensiert hat. Das Microsoft .NET Framework verwendet genau aus diesem Grunde sealed Klassen. So ist z.B. die Klasse String , die als compilerbekannte Klasse eine Sonderfunktion einnimmt, sealed. using System ; class Program { static void Main ( string [] args ) { AuxiliaryClass a = new AuxiliaryClass () ; } } sealed class AuxiliaryClass { // ... do something internal // to the library } 3.4 Partielle Klassen C# erlaubt, den Code für eine Klasse auf mehrere Files aufzuteilen. Visual Studio 2005 nutzt das zum Beispiel, um vom Form Designer automatisch erzeugten Code vom“handgeschriebenen” Code der Entwicklerin zu trennen. Das reduziert die sichtbare Komplexität und steigert die Übersichtlichkeit. Dabei tragen die einzelnen Klassen-Teile das Schlüselwort partial. using System ; partial class Program { static void Main ( string [] args ) { Console . WriteLine ( Quadrat (256) ) ; } } // ----- anderes File ------ partial class Program { static int Quadrat ( int x ) { return x * x ; } } 3.5 Vererbung C# unterstützt die einfache Vererbung. Ohnehin erbt jede Klasse von Object. using System ; class Program 3 Klassen 24 { static void Main ( string [] args ) { Azubi a = new Azubi () ; Console . WriteLine ( a ) ; } } class Person { protected string Nachname = " Müller " , Vorname = " Sven " ; protected int Alter =17; } class Mitarbeiter : Person { protected int Personalnummer = 1232; } class Azubi : Mitarbeiter { int Lehrjahr = 1; public override string ToString () { return " Azubi " + Vorname + Nachname + " im " + Lehrjahr + " . Lehrjahr " + " , Alter " + Alter + " Jahre " + " , Personalnummer " + Personalnummer ; } } Soll der Konstruktor einer Klasse den Konstruktor der Basisklasse aufrufen, so geschieht dies in der Klassendefinition. Im folgenden Beispiel ruft der Konstruktor von Mitarbeiter den Konstruktor der Basisklasse Person auf, um eine Code-Dopplung zu vermeiden: using System ; class Program { static void Main ( string [] args ) { Mitarbeiter m = new Mitarbeiter ( " Müller " , " Sven " , 17 , 32168) ; Console . WriteLine ( m ) ; } } class Person { protected string Nachname , Vorname ; protected int Alter ; protected Person ( string nachname , string vorname , int alter ) { this . Nachname = nachname ; this . Vorname = vorname ; this . Alter = alter ; } public override string ToString () 3.5 Vererbung 25 { return " Name : " + Vorname + " " + Nachname + " , Alter : " + Alter ; } } class Mitarbeiter : Person { protected int Personalnummer ; public Mitarbeiter ( string nn , string vn , int alter , int pn ) : base ( nn , vn , alter ) { this . Personalnummer = pn ; } public override string ToString () { return base . ToString () + " Personalnummer : " + Personalnummer ; } } 3.5.1 Der is-Operator Der is-Operator ist true, wenn ein Objekt vom gleichen Typ ist wie das angegebene Objekt oder davon abgeleitet. using System ; class Program { static void Main ( string [] args ) { Person m = new Mitarbeiter () ; Person p = new Person () ; if ( p is Person ) Console . WriteLine ( " Person ist Person " ) ; if ( m is Person ) Console . WriteLine ( " Mitarbeiter ist Person " ) ; if ( p is Mitarbeiter ) Console . WriteLine ( " Person ist Mitarbeiter " ) ; if ( m is Mitarbeiter ) Console . WriteLine ( " Mitarbeiter ist Mitarbeiter " ) ; } } class Person { protected string Nachname = " Müller " , Vorname = " Sven " ; protected int Alter =17; } class Mitarbeiter : Person 3 Klassen 26 { protected int Personalnummer = 1232; } ergibt folgende Ausgabe: Person ist Person Mitarbeiter ist Person Mitarbeiter ist Mitarbeiter 3.5.2 Der as-Operator Der as-Operator nimmt direkt eine Typumwandlung vor. Falls der Zieltyp vom gleichen Typ ist oder vom Typ einer Mutterklasse ist, wie der Ursprung, funktioniert die Konvertierung. Ansonsten ist das Ergebnis null. using System ; class Program { static void Main ( string [] args ) { Mitarbeiter m = new Mitarbeiter () ; Person p = new Person () ; Person tp = m as Person ; Mitarbeiter tm = p as Mitarbeiter ; Console . WriteLine ( " Testperson : " + tp ) ; Console . WriteLine ( " Testmitarbeiter : " + tm ) ; } } class Person { protected string Nachname = " Müller " , Vorname = " Sven " ; protected int Alter =17; } class Mitarbeiter : Person { protected int Personalnummer = 1232; } in diesem Fall gelingt die Konvertierung von Mitarbeiter Û Person mit Person tp = m as Person; aber die Konvertierung von Person Û Mitarbeiter mit Mitarbeiter tm = p as Mitarbeiter; misslingt. Daher gibt die zweite Ausgabezeile keinen weiteren Text ab: Testperson : Mitarbeiter Testmitarbeiter : 27 4 Methoden und Eigenschaften Methoden sind Klassenfunktionen. Funktionen außerhalb von Klassen kommen in C# nicht vor1 . Sie können die gleichen Zugriffsmodifikatoren wie Variable haben: public, private, protected, internal, und static. Auch sealed als Modifier ist möglich: Methoden, die andere Programmierer nicht überschreiben sollen, sind sealed. Das klingt zunächst seltsam, denn das Weglassen von virtual hätte auch gereicht, um Überschreiben zu verhindern. Bei abgeleiteten Methoden gibt es aber in einer Basisklasse eine Methode, die virtual ist, und wenn die Programmiererin in der abgeleiteten Klasse das weitere Ableiten verhindern will, kennzeichnet sie die Methode als selaed. using System ; class Program { static void Main ( string [] args ) { LimitedClass l = new LimitedClass () ; } } class FreeClass { public virtual void DoSomething () { Console . WriteLine ( " FreeClass " ) ; } } class LimitedClass : FreeClass { public sealed override void DoSomething () { base . DoSomething () ; } } 4.1 Methoden-Parameter Die Übergabe von Wertetypen als Methoden-Parameter erfolgt by-value - das Laufzeitsytem erstellt eine Kopie der Werte und übergibt sie auf dem Stack. Bei Referenztypen erstellt das Laufzeitsystem eine Kopie des Referenz-Zeigers, während das Objekt weiterhin nur einmal auf dem Heap liegt. Sollen Methodenparameter einen Rückgabewert tragen, so erfährt der Compiler das über die Kennzeichnungen ref und out. Normale Parameter übergeben Werte in die Methode, die bei Wertetypen durch Kopie aus den Orignalen auf dem Stack entstehen. Bei Referenztypen legt das Laufzeitsystem eine Kopie der Referenz auf den Stack - das Originalobjekt bleibt unkopiert. ref-Parameter übergeben Werte in die Methode und können Rückgabewerte 1 Einige Klasse definieren statische Methoden, die als Ersatz von globalen Funktionen dienen können. 4 Methoden und Eigenschaften 28 tragen. Hierbei achtet der C# -Compiler darauf, dass die Variablen vor der Übergabe initialisiert sind. out-Parameter tragen nur Rückgabewerte - das aufrufende Programm braucht sie nicht zu initialisieren. Die Programmiererin muss ref und out bei der Deklaration der Methode angeben und dann bei jedem Aufruf der Methode noch einmal. using System ; class Program { static void Main ( string [] args ) { int a = 3; int b = 2; int c ; int d = DoSomething (a , ref b , out c ) ; Console . WriteLine ( " a ={0} b ={1} c ={2} d ={3} " , a, b, c, d ); } static int DoSomething ( int x , ref int y , out int z ) { x += y ; y = 7; z = 6; return x ; } } Der aufrufende Programmteil übergibt Methoden-Parameter standardmäßig entweder als Wert (call-by-value), wenn es sich um Wertetypen handelt, oder über Referenzen (call-by-reference). Die Programiererin kann die Übergabe als Referenz erzwingen, wenn sie den Methoden-Parameter auch für die Rückgabe von Funktionswerten nutzen möchte. Speziell bei Objekten bedeutet das, dass sowohl bei Übergabe als Referenz aber auch als Wert der aufrufende Programmteil eine Kopie vom Zeiger auf das Objekt übergibt, ohne jedoch das Objekt selber zu duplizieren. Das Verhalten von Strings ist wiederum anders. Strings können sich nicht verändern und eine Manipulation des übergebenen Strings innerhalb einer Methode erzeugt ein neues String-Objekt und dadurch bleibt das String-Objekt im aufrufenden Programmteil unverändert. 4.2 Methoden mit variabler Parameterzahl C# bietet eine einfache Weise, Methoden mit einer variablen Anzahl Parametern gleichen Typs zu versorgen. Das params-Schlüsselwort vor dem letzten Argument einer Methodenparameterliste bewirkt, dass C# die beim Aufruf folgenden Parameterwerte in einem Array des angegebenen Typs übergibt. static void Main ( string [] args ) { int [] einArray = { 12 , 17 , 30 , 21 }; PrintArgs ( einArray ) ; PrintArgs (10 , 12 , 14 , 16) ; } static void PrintArgs ( params int [] args ) { foreach ( int i in args ) Console . WriteLine ( i ) ; } 4.3 Überschreiben von Methoden 29 Sogar das Mischen von Parametertypen bei Parameterlisten variabler Länge ist möglich, wenn der Typ der variablen Liste Object ist: static void Main ( string [] args ) { Object [] einArray = { 18 , 20 , " weg ! " }; PrintArgs ( einArray ) ; PrintArgs ( " Stuttgart " , 21) ; } static void PrintArgs ( params Object [] args ) { foreach ( Object o in args ) Console . WriteLine ( o ) ; } 4.3 Überschreiben von Methoden 4.3.1 Überschreiben virtueller Methoden In C# gibt das Schlüsselwort virtual an, dass die Programmiererin in abgeleiteten Klassen diese Methode durch eine eigene Methode überschreiben oder durch eine neue Methode ersetzen darf. Das Laufzeitsystem geht von einer überschriebenen Methode den Vererbungsweg rückwärts, bis sie eine Methode der gleichen Signatur gefunden hat, die virtual ist. Damit das Laufzeitsystem erkennt, dass das Überschreiben absichtlich erfolgt ist, muss die Programmiererin beim Überschreiben einer Methode durch eine eigene Methode dieses mit override angeben. Beim Ersetzen einer Methode kennzeichnet sie dies mit new. Überschriebene Methoden der Basisklassen kann die abgeleitete Klasse immer noch aufrufen, wenn sie das base-Schlüsselwort verwendet. Dabei sucht das Laufzeitsystem bei base immer die Vererbungshierarchie aufwärts, bis es die Methode findet und ruft diese dann auf. using System ; class Program { static void Main ( string [] args ) { Grossvater [] familie = { new Grossvater () , new Vater () , new Sohn () }; foreach ( Grossvater gi in familie ) Console . WriteLine ( gi . Info () ) ; } } class Grossvater { public virtual String Info () { return " Großvater " ; } } class Vater : Grossvater { public override String Info () { 4 Methoden und Eigenschaften 30 return base . Info () + " Vater " ; } } class Sohn : Vater { public new String Info () { return base . Info () + " Sohn " ; } } Wie das Ergebnis zeigt, überschreibt die Info()-Methode des Vaters diejenige des Großvaters. Daher ruft der Iterator sie beim Durchlaufen auch auf. Der Iterator ruft jedoch nicht die Info()Methode des Sohns auf, weil diese Methode die des Großvaters nicht überschreibt, sondern nur für die Verwendung und alle weiteren Ableitungen verdeckt. Großvater Großvater Vater Großvater Vater 4.3.2 Erweitern vorhandener Klassen (Extension-Methoden) Neu in C# 3.0 ist die Möglichkeit, vorhandene Klassen um eigene Methoden erweitern zu können. Dies war bisher nur dann möglich, wenn es sich um von der Programmiererin selbst geschriebene Klassen handelte und Zugriff auf den Quellcode möglich war. Speziell partielle Klassen (Schlüsselwort partial haben sich dafür angeboten, weil der Code für die Erweiterungen in andern Dateien liegen konnte. Das folgende Beispiel zeigt das Ergänzen der String-Klasse um den Reverse()-Aufruf, der einen umgedrehten String liefert: using System ; using System . Text ; namespace ExtensionMethodTest { class Program { static void Main ( string [] args ) { string aok = " All OK " ; Console . WriteLine ( " {0}: {1} " , aok , aok . Reverse () ) ; } } public static class ExtensionMethods { public static string Reverse ( this string s ) { StringBuilder sb = new StringBuilder () ; if ( s . Length > 0) for ( int i = s . Length -1; i >=0; i - -) sb . Append ( s [ i ]) ; return sb . ToString () ; } } } 4.4 Operator-Überschreiben 31 4.4 Operator-Überschreiben Das Überschreiben von Operatoren funktioniert in C# wie in C++ oder Java auch. Wer dabei den == Operator überschreibt, muss auch den != Operator überschreiben und muss dann noch Object.Equals(Object o) und Object.GetHashCode() überschreiben. using System ; class Program { static void Main ( string [] args ) { DemoClass a = new DemoClass () ; DemoClass b = new DemoClass () ; if ( a == b ) Console . WriteLine ( " a und b sind gleich " ) ; } } class DemoClass { public static bool operator == ( DemoClass a , DemoClass b ) { return true ; } public static bool operator !=( DemoClass a , DemoClass b ) { return false ; } public override bool Equals ( object obj ) { return base . Equals ( obj ) ; } public override int GetHashCode () { return base . GetHashCode () ; } } Beim Indexer-Operator [] geht das Überschreiben nicht. Statt dessen greift ein Programm auf den Indexer über die Property this auf den Indexer zu. 4.5 Eigenschaften (Properties) Propertis erlauben kontrollierteren Zugriff auf Klassen-Variable und dienen dem InformationHiding. Nach außen sieht eine Property aus wie eine Variable. Ein lesender oder schreibender Zugriff auf eine Property ruft allerdings eine Methode auf, die einen Wert setzt oder einen Wert zurückliefert. using System ; class Program { static void Main ( string [] args ) 4 Methoden und Eigenschaften 32 { Zeit t = new Zeit () ; t . Minute = 30; Console . WriteLine ( t ) ; t . Minute = 70; Console . WriteLine ( t ) ; } } class Zeit { int stunde , minute , sekunde ; // ... public int Minute { get { return minute ; } set { if ( value >= 0 && value <= 60) minute = value ; } } public override string ToString () { return String . Format ( " {0: D2 }:{1: D2 }:{2: D2 } " , stunde , minute , sekunde ) ; } } Mit Hilfe von Properties kann die Klasse so präzise überwachen, ob die Werte auf die das zugreifende Programmteil eine Variable setzen möchte überhaupt zulässig sind. Zum anderen kann die Klasse so die interne Repräsentation eines Wertes verstecken (Information Hiding). So kann sich die interne Repräsentation der Werte innerhalb der Klasse später ändern, ohne dass dies Code außerhalb der Klasse betrifft. 4.5.1 Automatische Eigenschaften Version 3.0 von C# erlaubt, Eigenschaften automatisch zu erstellen. Sie geben dazu nur public String Name { get ; set ;} und der Compiler erzeugt den Code für die Eigenschaft und den Zugriff auf eine dahinterliegende Variable. Dies entspricht der Verwendung einer öffentlichen Variablen und der Nutzen dieser Abkürzungsmöglichkeit aus Sicht von Microsoft ist nicht ganz einsichtig. Einzig die Möglichkeit, eine der Methoden get oder set als private zu deklarieren und die andere nicht unterscheidet diese Variante von der Verwendung einer öffentlichen Variablen. 4.6 Indexer Mit der Hilfe eines Indexers kann das aufrufende Programmteil eine Klasse ansprechen wie ein Array. Indexer sehen zunächst aus wie eine Property, aber sie verwenden this[...]. Der Index 4.6 Indexer 33 des auszulesenden Werts steht in den eckigen Klammern hinter this, der zu setzende Wert in value, einem reservierten Wort von C# . using System ; class Program { static void Main ( string [] args ) { } } class Schachfigur { // .... } class Schachfeld { private Schachfigur [ ,] feld ; public void Initialize () { // .... } public Schachfigur this [ int x , int y ] { get { return feld [x , y ]; } set { feld [x , y ] = value ; } } } Dabei sind die Indexparametertypen nicht auf Integerwerte beschränkt, es sind vielmehr beliebige Typen zulässig. Das führt zu einer ganz einfachen Implementation von assoziativen Arrays: using System ; class Program { static void Main ( string [] args ) { Rechteck r = new Rechteck (3 , 4) ; Console . WriteLine ( " Länge : {0} , Breite : {1} " , r [ " Länge " ] , r [ " Breite " ]) ; r [ " Breite " ] = 5; Console . WriteLine ( " Länge : {0} , Breite : {1} " , r [ " Länge " ] , r [ " Breite " ]) ; } } 4 Methoden und Eigenschaften 34 class Rechteck { double [] dimensions = new double [2]; public Rechteck ( double l , double b ) { dimensions [0] = l ; dimensions [1] = b ; } public double this [ String side ] { get { if ( side == " Länge " ) return dimensions [0]; else if ( side == " Breite " ) return dimensions [1]; else throw new Index OutOf RangeE xcept ion ( " Index nicht \" Breite \" oder \" Höhe \" " ) ; } set { if ( value < 0) throw new ArgumentException ( " Dimension kleiner Null " ) ; if ( side == " Länge " ) dimensions [0] = value ; if ( side == " Breite " ) dimensions [1] = value ; } } } 4.7 Signatur Der C# -Compiler hält überschriebene Methoden und Delegates über deren Signatur auseinander. Die Signatur einer Methode besteht aus ihrem Namen und der Zahl und dem Typ ihrer Argumente. Sie beinhaltet nicht den Rückgabewert oder eine params-Angabe für den letzten Parameter. Ebenfalls spielen Zugriffsbeschränkungen oder static bei Signatur keine Rolle. using System ; class Program { static void Main ( string [] args ) { Console . WriteLine ( " Die Summe ist : " + Sum (1 , 2) ) ; Console . WriteLine ( " Die Summe ist : " + Sum (1 , 2 , 3) ) ; } static int Sum ( int x , int y ) { return x + y ; } 35 4.8 Generische Methoden static int Sum ( int x , int y , int z ) { return x + y + z ; } } 4.8 Generische Methoden Eine Methode, die einen Typ-Paramenter hat, ist generisch. Der generische Typ steht dabei in spitzen Klammern – bei Methoden mit nur einem Typ heißt dieser oft “T”. Im folgenden Programm vertauscht die Methode Tausche() ihre beiden Parameter: using System ; class Program { static void Main ( string [] args ) { string sa = " Zucker " , sb = " Salz " ; int ia = 4 , ib = 8; Console . WriteLine ( " String a ={0} , b ={1} " , Console . WriteLine ( " int a ={0} , b ={1} " , Tausche < string >( ref sa , ref sb ) ; Tausche < int >( ref ia , ref ib ) ; Console . WriteLine ( " String a ={0} , b ={1} " , Console . WriteLine ( " int a ={0} , b ={1} " , } sa , sb ) ; ia , ib ) ; sa , sb ) ; ia , ib ) ; private static void Tausche <T >( ref T a , ref T b ) { T temp = a ; a = b; b = temp ; } } Die Ausgabe zeigt, dass die Parameter nun vertauscht sind: String int String int a = Zucker , b = Salz a =4 , b =8 a = Salz , b = Zucker a =8 , b =4 4.9 die Main-Methode In C# gibt die Main-Methode an, wo das Laufzeitsystem die Ausführung des Codes starten soll. Falls mehrere Klassen eine Main-Methode besitzen, muss die Anwenderin des Programms bei Start den gewünschten Klassennamen angeben. Dazu stellt sie unter“Project/Properties/Startup Object” die Klasse ein, deren Main()-Methode das Laufzeitsystem beim Programmstart aufruft. Im Gegensatz zu C++ ist diese Startmethode Main in C# groß geschrieben und immer statisch. using System ; class Program1 { 4 Methoden und Eigenschaften 36 static void Main ( string [] args ) { Console . WriteLine ( " Programm 1 " ) ; } } class Program2 { static void Main ( string [] args ) { Console . WriteLine ( " Programm 2 " ) ; } } 37 5 Interfaces Interfaces geben die öffentlichen Methoden und Properties an, die eine Klasse implementieren muss. Interfaces finden dann Verwendung, wenn es keine passende Klasse gibt, von der eine Klasse brauchbare Methoden und Properties erben kann. Während eine abstrakte Klasse dann das hierarchische Erben von zu implementierenden Methoden erzwingt, sind Interfaces eher geeignet, Klassen mit verschiedenen Aufgaben durch ihr Interface zusammenzubringen. Die Definition eines Interfaces beginnt mit dem Schlüsselwort interface, gefolgt von öffentlichen Methoden und Properties. Die Klasse, die das Interface verwendet, schließt damit einen Vertrag mit dem C# -Compiler, alle Methoden und Properties des Interfaces zu implementieren. Fehlt eine Methode oder Property bei der Implementierung, so führt das zu einem Compilerfehler. In C# sind nur public Interfaces zulässig. 5.1 IEnumerable IEnumerable stammt aus .NET 1.0 und verspricht eine einzige Funktion, GetEnumerator(), die eine Methode zurückliefert, die das Interface IEnumerator unterstützt: GetEnumerator() liefert einen Enumerator zurück, der Current, MoveNext() und Reset() unterstützt. Interface IEnumerable { IEnumerator GetEnumerator () ; } In .NET 2.0 bietet sich die Verwendung der typsicheren Variante dieses Interfaces an, IEnumerable<T> . 5.2 IEnumerator Das Interface IEnumerator unterstützt die Methoden von .NET 1.0 zum Durchlaufen von Sammlungen (Collections). Interface IEnumerator { object Current { get ; } bool MoveNext () ; void Reset () ; } Dabei haben die Methoden und Properties folgende Aufgaben: Current Vorsicht beim Cast liefert das aktuelle Objekt zurück. Diese Methode ist nicht typsicher, weil das von Object aufrufende Programm das gelieferte Objekt auf den wirklich gespeicherten Objekttyp casten wird - eine häufige Fehlerquelle. 5 Interfaces 38 MoveNext() geht weiter zum nächsten Objekt in der Sammlung und liefert true, falls es ein nächstes Objekt gibt. Falls ein nächstes Objekt nicht existiert, dann liefert MoveNext() false zurück. Reset() setzt den internen Zeiger in die Sammlung vor das erste Objekt, so dass das nächste MoveNext() das erste Objekt zurückgibt oder false zurückgeben kann, falls die Sammlung kein Element enthält. 5.3 IConvertible IConvertible implementiert Methoden, die ein Objekt der aktuellen Klasse explizit in eine andere Darstellung konvertieren (Boolean, Byte, Character, DateTime, Decimal, Double, Int16, Int32, Int64, SByte, Single, UInt16, Uint32, UInt64) 5.4 IComparable IComparable implementiert die Methode CompareTo(), die einen Vergleich mit einem andern Objekt bietet. Eigene Klassen überschreiben meist CompareTo() durch geeigneteren Code für einen Vergleich. CompareTo() bietet einen Vergleich mit andern Objekten. 5.5 ICloneable ICloneable implementiert die Methode Clone(), die eine weitere Referenz auf das gleiche Objekt zurückliefert. Das eigentliche Objekt bleibt dabei nur einfach im Speicher – es hat lediglich 2 Referenzen, die auf es zeigen. Clone() liefert neue Referenz auf bekanntes Objekt. 5.6 IFormatable Das Interface IFormatable überlädt die systemeigene parameterlose ToString()-Methode um eine Methode mit 2 Parametern: Interface IFormatable { string ToString ( string format , IFormatProvider formatProvider ) ; } ToString() benutzereigene Darstellung einer Klasse, passend zur formatierten Ausgabe mit String.Format() oder System.Console.WriteLine(). Das folgende Beispiel definiert einen Ausschnitt aus einer Klasse für komplexe Zahlen, die ein eigenes Formatierungskennzeichen, den Buchstaben “c” erhält: using System ; class Program { static void Main ( string [] args ) { 5.7 IEnumerable<T> 39 Complex a = new Complex () , b = new Complex (7.3 , 13) ; Console . WriteLine ( " a ist {0: c } " , a ) ; Console . WriteLine ( " b ist {0: c } " , b ) ; } } class Complex : IFormattable { public double re , im ; public Complex () { } public Complex ( double r , double i ) { re = r ; im = i ; } public string ToString ( string format , IFormatProvider formatProvider ) { if ( format == null ) return ToString () ; if ( format == " c " ) return String . Format ( " ({0} ,{1} i ) " , re , im ) ; return ToString () ; } // ... } Falls die Klasse Complex das Interface IFormatable angibt, dann sieht die Ausgabe folgendermaßen aus: a ist (0 ,0 i ) b ist (7 ,3 ,13 i ) 5.7 IEnumerable<T> Wenn das foreach-Konstrukt durch eine Sammlung laufen soll, dann muss diese Sammlung das Interface IEnumerable<T> unterstützen. Das Interface IEnumerable<T> verspricht, die Methode GetEnumerator() zu unterstützen: GetEnumerator() liefert einen Enumerator zurück, der Current, MoveNext() und Reset() unterstützt. Die generische Implementation erbt dabei von der nicht-generischen, objektbasierten Implementation von IEnumerable . 5 Interfaces 40 5.8 IList<T> Klassen, die das Interface IList<T> unterstützen, erlauben einen Zugriff auf die Klassenelemente, als ob es ein Array wäre. Darüberhinaus unterstützen die Methoden des Interfaces das Entnehmen und Einfügen von Elementen an beliebigen Positionen. Item Property zum Lesen und Setzen eines Array-Elements. bool IsFixedSize Property zum Feststellen, ob das Array feste Größe hat. bool IsReadOnly Property zum Feststellen, ob nur lesender Zugriff erlaubt ist. int Add(element) Fügt ein Element zur Liste hinzu. Liefert die Position des eingefügten Elements zurück. void Clear() Löscht alle Listenelemente. bool Contains(element) Durchsucht die Liste nach dem angegebenen Element und liefert zurück, ob es enthalten ist. int IndexOf(element) Durchsucht die Liste nach dem angegebenen Element und liefert dessen Position zurück, wenn es enthalten ist, andernfalls -1. void Insert(position, element) Fügt das Element an der angegebenen Position ein. In der Liste evtl. schon vorhandene Elemente rutschen um eine Position nach hinten. void Remove(object) Sucht das angegebene Element in der Liste und entfernt es. Die nachfolgenden Elemente rutschen um eine Position auf. Falls das Element nicht in der Liste vorhanden ist, passiert nichts. void RemoveAt(position) Entfernt das Element an der angegebenen Position. Die nachfolgenden Elemente rutschen um eine Position auf. 5.9 ICollection<T> Collection-Klassen implementieren das Interface ICollection. Die Methoden des Interfaces fügen Elemente zur Sammlung hinzu oder löschen sie. int Count Liefert die Anzahl der Elemente in der Sammlung. bool IsReadOnly Ist die Sammlung nur zum Lesen gedacht? void Add() Fügt ein Element zur Sammlung hinzu. void Clear() Löscht alle Elemente der Sammlung. Contains Durchsucht die Liste nach dem angegebenen Element und liefert zurück, ob es enthalten ist. void CopyTo(array, index) Kopiert alle Elmente der Liste ab einem bestimmten Index in das übergebene Array. bool Remove(element) Sucht das angegebene Element in der Liste und entfernt dessen erstes Auftreten. Liefert bei Erfolg true. 41 6 Strings In C# sind Strings Objekte einer eigenen Klasse System.String, der String-Klasse, die mehr Funktionen zur Verfügung stellt als C++ mit seinem String-Array. Da Programmiererinnen von String nicht ableiten sollen, ist die String-Klasse sealed . Strings in C# bestehen aus Unicode-Zeichen mit 2 Byte Größe. Einmal erzeugt sind sie unveränderlich. Das heißt, dass schon das einfache hinzufügen eines weiteren Zeichens zu einer Kopieroperation an einen neuen Ort im Speicher führt, den das Laufzeitsystem zuvor noch bereitstellen muss. Sonderzeichen innerhalb von Strings schützt die Programmiererin durch einen vorangestellten Rückwärtsstrich (backslash). Strings unter Windows enthalten oft recht viele Rückwärtsstriche , weil das Dateisystem das erfodert. Da der Rückwärtsstrich dasjenige Zeichen ist, das Sonderzeichen schützt, braucht es zwei aufeinanderfolgende Rückwärtsstriche, um einen Rückwärtsstrich in einen String einzubauen. C# hat daher zur Vereinfachung den “literalen” String eingeführt (gekennzeichnet durch ein @), in dem der Compiler alle Sonderzeichen im String ignoriert. using System ; class Program { static void Main ( string [] args ) { String dateiname1 = " C :\\\\ Windows \\ Temp \\ cs_log . txt " ; String dateiname2 = @ " C :\\ Windows \ Temp \ cs_log . txt " ; Console . WriteLine ( dateiname1 ) ; Console . WriteLine ( dateiname2 ) ; } } 6.1 Escape-Character Innerhalb fluss auf Zeichen \a \b \t \r \v \f \n \x20 \u0020 von Strings gibt es eine Reihe von Sonderzeichen, die speziell Eindie Darstellung oder Formattierung des Ausgabetextes haben. Bedeutung Akustisches Signal. Backspace. Tabulator. Carriage Return. Vertical Tabulator. Form Feed. Newline. ASCII-Zeichen in Hexadezimaldarstellung (genau 2 Hex-Zeichen). Unicode-Zeiche in Hexadezimaldarstellung (genau 4 Hex-Zeichen). Tabelle 6.1: Sonderzeichen in Strings 6 Strings 42 6.2 Interfaces Die Klasse String implementiert die Interfaces IEnumerable, IConvertible und erbt von Object insbesondere IComparable und ICloneable. IEnumerable Implementiert die Methode GetEnumerator, die einen Enumerator zurückliefert, der durch die einzelnen Zeichen eines Strings iterieren kann IConvertible Implementiert Methoden, die den Sting explizit in eine andere Darstellung konvertieren (Boolean, Byte, Character, DateTime, Decimal, Double, Int16, Int32, Int64, SByte, Single, UInt16, Uint32, UInt64) IComparable Implementiert die Methode CompareTo, die, je nach Überladung, einen Stringvergleich oder einen Vergleich mit einem andern Objekt bietet ICloneable Implementiert die Methode Clone, die eine weitere Referenz auf den gleichen Stringinhalt zurückliefert Die String-Klasse implementiert eine öffentlich sichtbare und nur lesbare Variable, den leeren String Empty. Weiterhin gibt es zwei öffentlich lesbare Properties, Chars, die das Zeichen an der angegebenen Position zurückliefert und Length, die die Länge des Strings in Zahl der Zeichen angibt. 6.3 String-Erzeugung Der Compiler erkennt einen literalen String1 und baut ihn zur Laufzeit in ein Objekt der Klasse System.String ein. Dieses Objekt unterstützt dann alle Methoden der Klasse String, wie zum Beispiel auch den überschriebenen “+”-Operator. using System ; class Program { static void Main ( string [] args ) { String gruss = " Hallo " ; gruss += " Welt ! " ; Console . WriteLine ( gruss ) ; } } Im vorangehenden Beispiel hat das Laufzeitsystem erst ein String-Objekt erzeugt und mit dem Text“Hallo“ gefüllt (inclusive dem nacheilenden Leerzeichen). Auch hat er“Welt!”in ein weiteres String-Objekt eingepackt. Dann hat der überschriebene “+”-Operator von gruss beide Strings hintereinander in ein neu erzeugtes String-Objekt gepackt und die Referenz gruss auf das neue Objekt umgebogen. 1 String, der als Text in Hochkommata steht: “Beispiel” 43 6.4 String-Methoden 6.4 String-Methoden Methode Clone() Compare() CompareOrdinal() CompareTo() Concat() Contains() Copy() CopyTo() EndsWith() Equals() Format() GetEnumerator() GetHashCode() GetType() GetTypeCode() IndexOf() IndexOfAny() Insert() Intern() IsInterned() IsNormalized() IsNullOrEmpty() Join() Aufgabe Liefert eine Objekt-Referenz auf den gleichen String zurück. Vergleicht die beiden übergebenen Strings und liefert eine Zahl größer Null zurück, wenn der erste String lexikalisch (nach aktueller Kultureinstellung) größer ist als der zweite. Dazu entsprechend 0 bei Gleichheit und eine Zahl <0, wenn der zweite String kleiner ist als der erste. Vergleicht die in den beiden Strings enthaltenen Zahlen und liefert größer(>0), gleich(0) oder kleiner(<0) zurück. Vergleicht den übergebenen String mit dem eigenen Stringobjekt. Hängt bis zu vier Strings aneinander und liefert den durch Aneinanderhängen entstandenen String zurück. Liefert true zurück, wenn der String im Argument im aktuellen String enthalten ist. Kopiert den übergebenen String und liefert ihn in einen neuen Stringobjekt zurück. Kopiert Zeichen ab einer gegebenen Zeichenposition im aktuellen String in ein Character-Array um. Liefert true zurück, wenn der aktuelle String mit dem als Argument übergebenen String endet. Prüft, ob zwei String-Objekte gleich sind (mit einem kultur-invarianten und groß-klein-berücksichtigenden Vergleich). Ersetzt den Platzhalter im Format-String durch eine im Format-String angegebene Darstellung der als weiteren Argumente übergebenen Objekte. Liefert einen Enumerator, der die einzelnen Zeichen eines Strings durchlaufen kann. Liefert einen möglichst string-eindeutigen Wert. Liefert eine Instanz von System.Type, mit deren Hilfe das Laufzeitsystem mehr über den String erfahren kann. Liefert einen Enumerator zurück, der den Typ angibt (welcher Wertetyp oder Objekt oder String). Gibt die Position des Auftretens des übergebenen Suchstring im aktuellen String an. Gibt die Position des ersten Auftretens irgendeines Zeichens aus dem übergebenen Sucharray im aktuellen String an. Fügt den übergebenen String an der angegebenen Position in den aktuellen String ein und liefert ein neues Stringobjekt mit dem Ergebnis zurück. C# speichert jeden String nur einmal (auch Literale) im internen Pool. Intern() liefert die Stringreferenz auf die interne Darstellung zurück. Prüft nach, ob der übergebene String im internen Pool gespeichert ist. Prüft, ob der String in Unicode-Normalform vorliegt. Prüft, ob der String leer ist oder eine Null-Referenz. Baut einen String aus einem übergebenen String-Array zusammen, jeweils getrennt durch den angegebenen Trenn-String. Fortsetzung auf der nächsten Seite Û 6 Strings 44 Methode LastIndexOf() LastIndexOfAny() Normalize() op_Equality() op_Inequality() PadLeft() PadRight() ReferenceEquals() Remove() Replace() Split() StartsWith() Substring() ToCharArray() ToLower() ToLowerInvariant() ToString() ToUpper() ToUpperInvariant() Trim() TrimEnd() TrimStart() Aufgabe Gibt die letzte Position des Auftretens des übergebenen Suchstring im aktuellen String an. Gibt die letzte Position des Auftretens irgendeines Zeichens aus dem übergebenen Sucharray im aktuellen String an. Wandelt den String in eine bestimmte Unicode-Darstellung um. Prüft auf Gleichheit von zwei Strings. Verwendet intern Equals(). Prüft auf Ungleichheit von zwei Strings. Verwendet intern Equals(). Schiebt einen String nacht rechts und füllt links auf die angegebene Länge mit dem angegebenen Zeichen auf. Schiebt einen String nacht links und füllt rechts auf die angegebene Länge mit dem angegebenen Zeichen auf. Prüft, ob zwei Referenzen auf das selbe Objekt zeigen. Löscht die angegeben Zahl von Zeichen ab der angegebenen Position. Ersetzt alle Vorkommen des einen Strings durch den andern String. Zerlegt einen String in ein String-Array, indem es bei bei den im übergebenen Character-Array enthaltenen Buchstaben schneidet. Die Trenn-Buchstaben sind in den Rückgabe-Strings nicht enthalten. Liefert true zurück, wenn der aktuelle String mit dem als Argument übergebenen String anfängt. Schneidet einen Teilstring aus dem aktuellen String aus und liefert ihn zurück. Liefert die einzelnen Zeichen im String als Elemente eines CharacterArray zurück. Wandelt den String in Kleinbuchstaben. Wandelt den String in Kleinbuchstaben mit den Ersetzungsregeln der invarianten Kultur. Liefert eine Referenz auf die gleiche Instanz des Strings zurück. Keine Konvertierung etc. Wandelt den String in Großbuchstaben. Wandelt den String in Großbuchstaben mit den Ersetzungsregeln der invarianten Kultur. Entfernt alle Vorkommen von im Character-Array übergebenen Zeichen von Beginn und Ende des Strings. Entfernt alle Vorkommen von im Character-Array übergebenen Zeichen von Ende des Strings. Entfernt alle Vorkommen von im Character-Array übergebenen Zeichen von Beginn des Strings. Tabelle 6.2: Methoden von String 6.4.1 String-Vergleiche Zum Vergleichen von Strings eignen sich die Methoden Compare(), CompareOrdinal(), CompareTo(), Equals(), EndsWith() und StartsWith(): using System ; class Program { static void Main ( string [] args ) { string muster = " 123 gna gna gna 456 " ; 6.4 String-Methoden 45 string vgl = " 123 " ; Console . WriteLine ( String . Compare ( muster , vgl ) ) ; Console . WriteLine ( String . CompareOrdinal ( muster ,0 , vgl , 0 , 3) ) ; Console . WriteLine ( muster . CompareTo ( vgl ) ) ; Console . WriteLine ( muster . Equals ( vgl ) ) ; Console . WriteLine ( muster . StartsWith ( vgl ) ) ; Console . WriteLine ( muster . EndsWith ( vgl ) ) ; } } ergibt folgende Ausgabe: 1 0 1 False True False 6.4.2 String-Suchen Zum Suchen von Zeichen und Zeichenfolgen innerhalb von Strings dienen IndexOf(), IndexOfAny(), LastIndexOf(), and LastIndexOfAny(): using System ; class Program { static void Main ( string [] args ) { string muster = " 123 gna gna gna 456 " ; char [] pat = { ’3 ’ , ’g ’ , ’5 ’ }; Console . WriteLine ( muster . IndexOf ( " gna " ) ) ; Console . WriteLine ( muster . IndexOfAny ( pat ) ) ; Console . WriteLine ( muster . LastIndexOf ( " gna " ) ) ; Console . WriteLine ( muster . IndexOfAny ( pat ) ) ; } } ergibt folgende Ausgabe: 4 2 12 2 6.4.3 Kopieren von Strings Copy() und CopyTo() kopieren Strings in Strings oder Character-Arrays: using System ; class Program { static void Main ( string [] args ) { string muster = " 123 gna gna gna 456 " ; 6 Strings 46 string kopie = String . Copy ( muster ) ; char [] einzeln = { ’ ’, ’ ’, ’ ’, ’ ’, ’ ’, }; muster . CopyTo (0 , einzeln , 0 , 5) ; Console . WriteLine ( kopie ) ; Console . WriteLine ( einzeln ) ; } } ergibt folgende Ausgabe: 123 gna gna gna 456 123 g 6.4.4 Zerlegen und Zusammenbauen von Strings Substring() und Split() zerlegen Strings in Teile und Concat() und Join() bauen sie wieder zusammen: using System ; class Program { static void Main ( string [] args ) { string muster = " 123 gna gna gna 456 " ; string s1 = muster . Substring (4 , 7) ; // Split () - Argument nutzt params string [] sa2 = muster . Split ( ’ ’, ’2 ’) ; string s3 = String . Join ( " + " , sa2 ) ; string s4 = String . Concat ( muster , s3 ) ; Console . WriteLine ( s1 ) ; foreach ( string s in sa2 ) Console . WriteLine ( s ) ; Console . WriteLine ( s3 ) ; Console . WriteLine ( s4 ) ; } } füht zu: gna gna 1 3 gna gna gna 456 1+3+ gna + gna + gna +456+ 123 gna gna gna 456 1+3+ gna + gna + gna +456+ 6.4.5 Ersetzen und Verändern von String-Inhalten Insert(), Replace(), Remove(), PadLeft(), PadRight(), Trim(), TrimEnd() und TrimStart() verändern String-Inhalte und liefern einen neuen String zurück: 47 6.4 String-Methoden using System ; class Program { static void Main ( string [] args ) { string muster = " 123 gna gna gna 456 " ; string [] sa = new string [8]; sa [0] = muster ; sa [1] = muster . Insert (4 , " bla " ) ; sa [2] = muster . Replace ( " gna " , " bla " ) ; sa [3] = muster . Remove (3 , 14) ; sa [4] = muster . PadRight (24 , ’+ ’) ; sa [5] = sa [4]. PadLeft (28) ; sa [6] = sa [5]. Trim () ; sa [7] = sa [6]. TrimEnd ( ’+ ’) ; foreach ( string s in sa ) Console . WriteLine ( s ) ; } } ...ergibt folgende Ausgabe (mit angezeigten Leerzeichen): 123 gna 123 bla 123 bla 12356 123 gna 123 123 gna 123 gna gna gna 456 gna gna gna 456 bla bla 456 gna gna gna gna gna gna gna gna 456 ++++ gna 456 ++++ 456 ++++ 456 6.4.6 Groß- und Kleinschreibung von Strings ToLower(), ToLowerInvariant(), ToUpper() und ToUpperInvariant() verändern Groß- zu Kleinschreibung und umgekehrt: using System ; class Program { static void Main ( string [] args ) { string muster = " 123 Gna Gna Gna 456 " ; Console . WriteLine ( muster ) ; Console . WriteLine ( muster . ToLower () ) ; Console . WriteLine ( muster . ToUpper () ) ; } } ergibt 123 Gna Gna Gna 456 123 gna gna gna 456 123 GNA GNA GNA 456 48 6 Strings 6.4.7 String-Formatierung Format() schreibt benutzerdefinierte Darstellungen von Werten in Strings hinein. Mehr über die möglichen Formatangaben steht im Kapitel 6.5. 6.4.8 Länge eines Strings und Zugriff auf Einzelbuchstaben Length() liefert die Länge eines Strings zurück. Zugriff auf einen einzelnen Buchstaben bietet der Indexer. Durch ihn verhält sich ein String wie ein Character-Array, wenn das Laufzeitsystem ihn mit der Array-Notierung anspricht: using System ; class Program { static void Main ( string [] args ) { string muster = " Muster = Pattern " ; // Property statt Methode int musterLaenge = muster . Length ; for ( int i = 0; i < musterLaenge ; i += 2) // Indexer Console . Write ( muster [ i ]) ; Console . WriteLine () ; } } ergibt die einzeilige Ausgabe Mse = atr 6.5 Format-Angaben In vielen Fällen ist die Standard-Ausgabe von ToString() nicht so, wie sie aussehen soll. C# bietet die Möglichkeit, zu eigenen Klassen benutzerdefinerte Formate anzugeben. Dies könnte z.B. bei einer eigenen komplexen Klasse der Fall sein, oder bei einer Klasse, die Boote und Fähren beschreibt. Die Klasse unterstützt dazu das Interface IFormatable. Damit weiß das Laufzeitsystem, dass die Klasse eine erweiterte Form der Methode ToString unterstützt. 6.5.1 Standard-Formatangaben Das folgende Beispiel zeigt die Verwendung der eingebauten Standard-Formatangaben bei der Ausgabe von Zahlen mit WriteLine(): using System ; class Program { static void Main ( string [] args ) { double d = 1.23456; int i = 314; Console . WriteLine ( " d ={0 ,15: E } , i ={1 ,4} " , d , i ) ; } } 49 6.5 Format-Angaben mit der Ausgabe d= 1 ,234560 E +000 , i = 314 Die Argumente zu WriteLine() sind zum einen der Formatstring, zum andern die Variablen, die WriteLine() darstellen soll. Im Formatstring befinden sich, in geschweifte Klammern eingeschlossen, die Platzhalter, an deren Stelle das Laufzeitsystem bei der Ausgabe die formatierten Zahlen setzt. Der Platzhalter besteht aus folgenden Komponenten: Der Nummer des Arguments in der nachfolgenden Argumentenliste, beginnend bei 0. Einer der häufigsten Gründe für Exceptions ist, zu beim Umstellen der Argumenteliste das Häufiger Anpassen der Argumentenummer zu vergessen. Programmierfehler Der Länge der Darstellung der Ausgabe dieses Elements, durch ein Komma getrennt von der führt zu Argumentennummer. Im vorigen Beispiel ist die angegebene Länge in beiden Fällen (Double Exceptions und Integer) größer als nötig und das Laufzeitsystem hat mit Leerzeichen aufgefüllt. Der Formatspezifikation, durch einen Doppelpunkt von den vorangehenden Elementen getrennt. Tabelle 6.5.1 listet die Formatspezifikatoren auf: Außer der Argumentennummer sind alle andern Angaben optional. Spezifikator c D E F G N P X Wirkung Länderabhängiger Geldwert Normale Integerzahl Wissenschaftliche Darstellung Festkommazahl Normale Zahl Länderspezifisches Zahlenformat Prozentangabe Hexadezimalzahl Tabelle 6.3: Formatspezifikatoren Das Beispiel zeigt die verschiedenen Ausgaben bei Standardlängeneinstellung: using System ; class Program { static void Main ( string [] args ) { double d = 123456.789; int i = 314; Console . WriteLine ( " c : {0: c } " , d ) ; Console . WriteLine ( " D : {0: D } " , i ) ; Console . WriteLine ( " E : {0: E } " , d ) ; Console . WriteLine ( " F : {0: F } " , d ) ; Console . WriteLine ( " G : {0: G } " , d ) ; Console . WriteLine ( " N : {0: N } " , d ) ; Console . WriteLine ( " P : {0: P } " , d ) ; Console . WriteLine ( " X : 0 x {0: X } " , i ) ; } } Console.WriteLine() ruft dabei intern String.Format() auf, das die Formatierungsarbeiten übernimmt: 6 Strings 50 c: D: E: F: G: N: P: X: 123.456 ,79 ? 314 1 ,234568 E +005 123456 ,79 123456 ,789 123.456 ,79 12.345.678 ,90% 0 x13A In diesem Beispiel zu beachten: Die Console kann Euro-Zeichen nicht korrekt darstellen. 6.6 Reguläre Ausdrücke Reguläre Ausdrücke erleichtern Such- und Ersetzungs-Aufgaben, indem sie vordefinierte Muster und Suchverfahren bieten. Typische Einsatzgebiete der Regulären Ausdrücke sind: Vereinheitlichen der unterschiedlichen Schreibweisen von Telefonnummern in ein gemeinsames Format. Wortwiederholungen in Dokumenten erkennen und entfernen, wie sie typischerweise beim häufigen Umstellen von Sätzen in Skripten oder Diplomarbeiten entstehen. Alle Anfangsbuchstaben von Wörtern groß schreiben, z.B. bei den ID-Tags in einer Sammlung mit .mp3-Dateien. Jeden Satz mit einem Großbuchstaben beginnen lassen. Einen Uniform Resource Identifier (URI) in seine Bestandteile zerlegen, z.B.: http://meier:[email protected]:8080/info.php?docid=17&format=pdf Das System der Regulären Ausdrücke besteht aus zwei Komponenten: 1. Ein Satz von Escape Codes, die bestimmte Zeichenarten angeben: z.B. Ziffern, Punktuation, Buchstaben, Großbuchstaben, Wort- und Satzgrenzen . . . 2. Eine Methode, Teilstrings, Zwischenresultate und Endergebnisse zu gruppieren. Das folgende Beispiel sucht alle Vorkommen des Strings Ausdrücke und gibt die Positionen des Vorkommens des Strings als Collection zurück: using System ; using System . Text . RegularExpressions ; class Program { static void Main ( string [] args ) { string input = @ " Reguläre Ausdrücke erleichtern Such - und Ersetzungs - Aufgaben , indem sie vordefinierte Muster und Suchverfahren bieten . Typische Einsatzgebiete der Regulären Ausdrücke sind : " ; string pattern = " Ausdrücke " ; MatchCollection matches = Regex . Matches ( input , pattern ) ; 51 6.6 Reguläre Ausdrücke foreach ( Match m in matches ) Console . WriteLine ( m . Index ) ; } } ergibt folgende Ausgabe: 9 162 Etwas kompliziertere Muster aus einzelnen Symbolen finden fast beliebige Teilstrings innerhalb des gegebenen Inputs. Ein solches Muster besteht aus normalen Zeichen wie Buchstaben, Ziffern oder Satzzeichen sowie Symbolen mit Sonderbedeutung. Die folgende Tabelle gibt eine Übersicht über die C# bekannten Symbole: Symbol . * ^ Bedeutung Beliebiger Buchstabe außer Zeilenende Vorhergehendes Zeichen oder Gruppe 0mal, 1-mal oder beliebig oft wiederholt Vorhergehendes Zeichen oder Gruppe 1mal oder beliebig oft wiederholt Vorhergehendes Zeichen oder Gruppe 0mal oder 1-mal wiederholt Mindestens n und höchstens m Wiederholungen des vorangehenden Zeichens oder der vorangehenden Gruppe. Mindestens n Wiederholungen des vorangehenden Zeichens oder der vorangehenden Gruppe. Genau n Wiederholungen des vorangehenden Zeichens oder der vorangehenden Gruppe. Runde Klammern fassen Gruppen zusammen. Gleichzeitig ist alles innerhalb einer Gruppe im Suchergebnis enthalten (siehe Beispiel) Textanfang $ Textende \d \D \s \S \b \B Eine Ziffer Alle Zeichen, die keine Ziffer sind Whitespace (Leerzeichen, Tab, . . . ) Alle Zeichen, die kein Whitespace sind Wortgrenze Alle Zeichen, die keine Wortgrenze sind + ? {n,m} {n, } {n} (xxx ) Beispiel de. I* I* Wirkung der, des, dem, . . . leer, I, II, . . . leer, I, II, . . . IB?M IBM und IM a{3,4} aaa, aaaa a{2,} aa, aaa, . . . a{5} aaaaa (ein)+deutig eineindeutig, nicht eindeutig ^Das Das, aber nur wenn es am Textanfang steht hat., aber nur wenn es am Textende steht DIN-A4, DIN-A5, . . . AT, AE, . . . ein, \tein, \nein, . . . mein, nein, . . . ein, aber nicht dein ei in Wortmitte, z.B. mein, nein, aber nicht ein. hat$ DIN\-A\d A\D \sein \Sein \bein \Bei\B Tabelle 6.4: RegEx-Symbole Zur Suche nach Symbolen selbst im Text schützt man die Symbole durch einen vorgestellten Rückstrich (’\’). Beispiel für die Suche nach ganzen Wörtern. Das Suchmuster besteht aus mehr als einem oder mehreren (’cd+’) nichtleeren Zeichen (’\S’), gefolgt von einem Leerzeichen (’\s’). using System ; 6 Strings 52 using System . Text . RegularExpressions ; class Program { static void Main ( string [] args ) { string input = @ " Reguläre Ausdrücke erleichtern Such - und Ersetzungs - Aufgaben , indem sie vordefinierte Muster und Suchverfahren bieten . Typische Einsatzgebiete der Regulären Ausdrücke sind : " ; string pattern = @ " (\ S +) \ s " ; MatchCollection matches = Regex . Matches ( input , pattern ) ; foreach ( Match m in matches ) Console . WriteLine ( m . ToString () ) ; } } Ergibt nachstehende Ausgabe. Das letzte Wort im Eingabetext kommt dabei im Ausgabetext nicht vor, weil hinter ihm kein Leerzeichen folgt. Satzzeichen, die direkt auf Worte folgen, fallen mit unter das nicht-leere Suchmuster und tauchen im Ausgabetext wieder mit auf. Reguläre Ausdrücke erleichtern Such und Ersetzungs - Aufgaben , indem sie vordefinierte Muster und Suchverfahren bieten . Typische Einsatzgebiete der Regulären Ausdrücke Da C# bei regulären Ausdrücken immer den längsten Match sucht, liefert eine kleine Änderung alle Worte, wie gewünscht. string pattern = @ " (\ S +) " ; Das Einschränken der Zeichen auf nur alfabetische Zeichen durch die Liste in eckigen Klammern string pattern = @ " ([ A - Z Ä Ö Üa - zäöüß ]+) " ; ergibt endlich die gewünschte Liste von Wörtern. Eine kleine Änderung liefert die Hauptwörter: string pattern = @ " ([ A - Z Ä Ö Ü ][ a - zäöüß ]+) " ; liefert 53 6.7 Effiziente Stringbehandlung Reguläre Ausdrücke Such Ersetzungs Aufgaben Muster Suchverfahren Typische Einsatzgebiete Regulären Ausdrücke 6.7 Effiziente Stringbehandlung Die String-Methoden erzeugen bei jeder Veränderung eines Strings eine neue Kopie auf dem Heap und allozieren dafür Speicherplatz, den der Garbage-Collector später wieder freigeben muss. Die StringBuilder-Klasse verwaltet Zeichenketten intern in einem großzügig dimensionierten Puffer, den die Klasse selten vergrößern muss. Die StringBuilder-Klasse verwaltet den internen Puffer selbst und vergrößert ihn bei Bedarf, immer in Zweierpotenzen. StringBuilder hat einen mehrfach überladenen Konstruktor, der z.B. einen String als Argument akzeptiert und/oder eine Längenangabe für die erste Pufferplatzreservierung. Die folgende Tabelle gibt die Properties und Methoden der StringBuilder-Klasse an: Methode Capacity Chars Length MaxCapacity Append() AppendFormat() AppendLine() CopyTo() EnsureCapacity() Equals() GetHashCode() GetType() Insert() Aufgabe Liest oder setzt die aktuelle Puffergröße (darf nicht kleiner sein als die Stringlänge). Der Indexer: Auf StringBuilder-Instanzen kann man über die ArraySchreibweise lesend und schreibend zugreifen. Kürzt den String auf die angegebene Länge bzw. verlängert ihn mit Leerzeichen. Liefert Maximalgröße des Puffers. In einigen Implementationen ist dies der mit einer Int32 maximal darstellbare Wert. Hängt die Stringdarstellung des übergebenen Objekts an die Darstellung des aktuellen Objekts an. Hängt die Stringdarstellung des übergebenen Formatstrings und seiner Argumente an die Darstellung des aktuellen Objekts an. Hängt ein Zeilenende oder den übergebenen String gefolgt von einem Zeilenende an. Kopiert einen Teilstring und gibt ein Character-Array zurück. Vergrößert den internen Buffer auf mindestens den gewünschten Kapazitätswert, falls nötig. Vergleicht auf Gleichheit mit einem Objekt oder einer StringBuilderInstanz. Liefert einen möglichst string-eindeutigen Wert. Liefert eine Instanz von System.Type, mit deren Hilfe das Laufzeitsystem mehr über den StringBuilder erfahren kann. Fügt den übergebenen String an der angegebenen Position in den aktuellen String ein. Fortsetzung auf der nächsten Seite Û 6 Strings 54 Methode ReferenceEquals() Remove() Replace() ToString() Aufgabe Prüft, ob zwei Referenzen auf das selbe Objekt zeigen. Löscht die angegeben Zahl von Zeichen ab der angegebenen Position. Ersetzt alle Vorkommen des einen Strings durch den andern String. Konvertiert den Pufferinhalt des StringBuilder-Objekts in einen String. Tabelle 6.5: Methoden und Properties von StringBuilder 55 7 Collections Oft gehören mehrere Objekte logisch zusammen in eine Sammlung (Collection). Das array ist die wohl bekannteste Collection. Mit der Einführung von .NET 2.0 stehen der Programmiererin eine Reihe neuer typisierter Sammlungen zur Verfügung: List, Dictionary, SortedList, LinkedList, Queue und Stack. Sie finden sich im Namespace System.Collections.Generic. 7.1 List<T> Die Klasse List<T> ist eine array-ähnliche Sammlung von Objekten, die dynamisch wachsen oder schrumpfen kann. Sie implementiert die Interfaces IList<T>, ICollection<T> und IEnumerable<T>. Darüber hinaus unterstützt sie Sortieren und Umdrehen der Sammlung. Methode Capacity int Count Item void Add(element) void AddRange(collection) int BinarySearch(element) void Clear() bool Contains(element) void CopyTo(array, index) bool Exists(Bedingung<T>) Knoten Find(element) List<T> FindAll(Bedingung<T>) int FindIndex(Bedingung<T>) T FindLast(Bedingung<T>) int FindLastIndex(Bedingung<T>) void ForEach(Action<T>) Aufgabe Liest oder schreibt die maximale Zahl der Listeneinträge, die die Liste ohne Größenveränderung fassen kann. Liefert die Zahl der Listeneinträge zurück Liest oder setzt ein Element an einer bestimmten Position: Der Indexer. Hängt ein Element an die Liste an. Hängt alle Elemente in der Sammlung ans Ende der Liste Sucht das Element in der Liste. Leert die Liste. Stellt fest, ob das Element in der Liste ist. Kopiert alle Elmente der Liste ab einem bestimmten Index in das übergebene Array. Stellt fest, ob die Liste Elemente enthält, die der angegebenen Bedingung genügen. Findet den ersten Knoten, der das Element speichert. Liefert eine Liste mit allen Elementen, die der angegebenen Bedingung genügen. Liefert die Position des Elements, das der angegebenen Bedingung genügt. Liefert das letzte Element, das der angegebenen Bedingung genügt. Liefert die Position des letzten Elements, das der angegebenen Bedingung genügt. Führt das angegebene Action-Delegate für jedes Element der Liste durch. Fortsetzung auf der nächsten Seite Û 7 Collections 56 Methode List<T> GetRange(index, count) int IndexOf(element)) void Insert(position, element) void InsertRange(position, sammlung) int LastIndexOf(element) bool Remove(element) int RemoveAll(bedingung) void RemoveAt(position) void RemoveRange(position, anzahl) voidReverse() void Sort() T[] ToArray() void TrimExcess(groeße) bool TrueForAll(bedingung) Aufgabe Liefert eine flache Kopie der Elemente aus dem angegebenen Teil der Liste. Gibt die Position des ersten Auftretens des ELements in der Liste zurück. Fügt das Element an der angegebenen Position ein. Fügt eine Sammlung von Elementen an der angegebenen Position ein. Gibt die Position des letzten Auftretens des Elements in der List an. Entfernt das erste Auftreten des Elements aus der Liste und liefert bei Erfolg true. Entfernt alle Elemente, die einer bestimmten Bedingung gehorchen und liefert die Zahl der entfernten Elemente zurück. Entfernt das Element an der angegebenen Position. Entfernt eine gegebene Anzahl von Elementen ab einer bestimmten Position. Dreht die Reihenfolge der Elemente in der Liste um. Sortiert die Elemente der Liste. Überladen für eigene Sortierkriterien. Liefert die Elemente der Liste als Array zurück. Kürzt die Liste auf die angegebene Größe, wenn sie zu weniger als 90% gefüllt ist. Prüft, ob eine bestimmte Bedingung für alle Elemente der Liste erfüllt ist. Tabelle 7.1: Wichtigste Methoden und Properties von List<T> Im folgenden Beispiel nimmt die neue Instanz der List-Klasse nur Elemente vom Typ Punkt auf. Da die Klasse IEnumerable unterstützt, funktioniert das foreach-Konstrukt bei Console.WriteLine(). Das Überschreiben von Equals() ist notwendig, damit das Laufzeitsystem beim Suchen nach einem Element bei Remove() nicht nur die Referenzen vergleichen kann, sondern auch auf die Inhalte achten. using System ; using System . Collections . Generic ; class Program { static void Main ( string [] args ) { List < Punkt > Dreieck = new List < Punkt >() ; Punkt p3 = new Punkt (5 ,6) ; Dreieck . Add ( new Punkt (3 , 4) ) ; Dreieck . Add ( new Punkt (1 , 2) ) ; Dreieck . Add ( p3 ) ; foreach ( Punkt p in Dreieck ) Console . WriteLine ( " Punkt : {0: p } " , p ) ; Dreieck . Remove ( new Punkt (3 , 4) ) ; 7.1 List<T> 57 System . Console . WriteLine () ; foreach ( Punkt p in Dreieck ) Console . WriteLine ( " Punkt : {0: p } " , p ) ; Dreieck . Remove ( p3 ) ; System . Console . WriteLine () ; foreach ( Punkt p in Dreieck ) Console . WriteLine ( " Punkt : {0: p } " , p ) ; } } class Punkt : IFormattable { public double x , y ; public Punkt ( double x , double y ) { this . x = x ; this . y = y ; } public string ToString ( string format , IFormatProvider formatProvider ) { if ( format == null ) return ToString () ; if ( format == " p " ) return String . Format ( " ({0} ,{1}) " , x, y); return ToString () ; } public override bool Equals ( object obj ) { Punkt other = obj as Punkt ; return ( other . x == this . x && other . y == this . y ) ; } } ergibt folgende Ausgabe: Punkt : (3 ,4) Punkt : (1 ,2) Punkt : (5 ,6) Punkt : (1 ,2) Punkt : (5 ,6) Punkt : (1 ,2) Selbstgeschriebene Bedingungen beim Durchlaufen der Liste haben den Returntyp bool und als Argumenttyp den Typ eines Listenelements: using System ; using System . Collections . Generic ; class Program { static void Main ( string [] args ) { 7 Collections 58 List < string > Liste = new List < string >() ; Liste . Add ( " Ene " ) ; Liste . Add ( " Mene " ) ; Liste . Add ( " Muh " ) ; Liste . Add ( " und drauß bist du ! " ) ; foreach ( string s in Liste ) Console . WriteLine ( s ) ; Liste . RemoveAll ( ContainsE ) ; Console . WriteLine () ; foreach ( string s in Liste ) Console . WriteLine ( s ) ; } private static bool ContainsE ( string s ) { return s . Contains ( " e " ) ; } } ergibt folgende Ausgabe: Ene Mene Muh und drauß bist du ! Muh und drauß bist du ! 7.2 LinkedList<T> In der LinkedList<T> trägt jedes Element außer seinem Wert jeweils eine Referenz auf das davor kommende und auf das danach folgende Element. Das ermöglicht, zur Laufzeit schnell Elemente aus dem Innern der Liste entfernen oder neue Elemente hinzufügen zu können. Anders als z.B. bei der Queue brauchen hier die andern Elemente nicht aufzurücken oder Platz zu machen. Dafür erfolgt der Zugriff auf Elemente im Innern der Liste langsamer, weil sich das Laufzeitsystem erst über die Vorwärts- bzw. Rückwärts-referenzen bis zu der gewünschten Stelle hinhangeln muss. Die LinkedList hat 3 Properties zum Zugriff auf den Anfang, das Ende und die Elementzahl der Liste sowie eine Reihe von Methoden zum Einfügen, Löschen und Suchen in der Liste. Die folgende Aufstellung zeigt die wichtigsten von ihnen: Methode int Count T First T Last T AddAfter(knoten, element) T AddBefore(knoten, element) Aufgabe Liefert die Zahl der Listeneinträge zurück Referenz auf den Listenanfang Referenz auf das Listenende Fügt ein neues Element oder einen neuen Knoten nach dem angegebenen Knoten in die LinkedList ein. Fügt ein neues Element oder einen neuen Knoten vor dem angegebenen Knoten in die LinkedList ein. Fortsetzung auf der nächsten Seite Û 59 7.2 LinkedList<T> Methode void AddFirst(element) Aufgabe Fügt ein Element oder einen Knoten als erstes Element der Liste ein. Fügt ein Element oder einen Knoten als letztes Element der Liste ein. Leert die Liste Prüft, ob ein Element in der Liste gespeichert ist. Kopiert alle Elmente der Liste ab einem bestimmten Index in das übergebene Array. Findet den ersten Knoten, der das Element speichert. Findet den letzten Knoten, der das Element speichert. Entfernt das erste Auftreten des Elements aus der Liste und liefert true, falls die Operation erfolgreich war. Entfernt das erste Element der Liste Entfernt das letzte Element der Liste void AddLast(element) void Clear() bool Contains(element) void CopyTo(array, index) Knoten Find(element) Knoten FindLast(element) bool Remove(element) void RemoveFirst() void RemoveLast() Tabelle 7.2: Methoden und Properties von LinkedList<T> Die einzelnen Elemente der LinkedListNode<T> Previous stecken dabei in Objekten vom Typ LinkedListNode<T>, die ihrerseits Properties zum Zugriff auf den Element-Inhalt sowie das vorhergehende und nachfolgende Element bieten: LinkedList<T> List Liefert die LinkedList zurück, zu der der Knoten gehört. LinkexListNode<T> Next Ergibt das nachfolgende Element in der Liste. LinkedListNode<T> Previous Ergibt das vorhergehende Element in der Liste. T Value Liefert das in der Liste gespeicherte Element. Das folgende Beispielprogramm Fügt mit AddLast() das erste Element (1,1) der Liste ein und merkt sich dieses Element in Knoten n. Hängt mit AddLast() das nächste Element (2,2) an die Liste an Hängt mit AddLast() noch ein Element (3,3) an die Liste an Fügt nach dem ersten Element (1,1) mit AddAfter() das vierte Element (4,4) ein Stellt vor das allererste Element der Liste mit AddFirst() das fünfte Element (5,5) using System ; using System . Collections . Generic ; class Program { static void Main ( string [] args ) { LinkedList < Complex > Liste = new LinkedList < Complex >() ; LinkedListNode < Complex > n ; n = Liste . AddLast ( new Complex (1 , 1) ) ; Liste . AddLast ( new Complex (2 , 2) ) ; Liste . AddLast ( new Complex (3 , 3) ) ; Liste . AddAfter (n , new Complex (4 , 4) ) ; Liste . AddFirst ( new Complex (5 , 5) ) ; 7 Collections 60 foreach ( Complex c in Liste ) Console . WriteLine ( c ) ; } } class Complex { protected float re , im ; public Complex ( float re , float im ) { this . re = re ; this . im = im ; } public override string ToString () { return " ( " + re + " ," + im + " i ) " ; } } Die Liste hat danach folgenden Aufbau (5 ,5 i ) (1 ,1 i ) (4 ,4 i ) (2 ,2 i ) (3 ,3 i ) 7.3 Queue<T> Die Queue<T>-Klasse implementiert einen First-In-First-Out-Speicher (FiFo) oder auch Warteschlange. Die wichtigsten Methoden und Properties von Queue<T> sind: Methode Count void Clear() bool Contains(element) void CopyTo(array, index) T Dequeue() void Enqueue(element) T Peek() T[] ToArray() void TrimExcess() Aufgabe Property, die die Zahl der Einträge in der Schlange zurückliefert Löscht alle Elemente in der Schlange. Prüft, ob ein Element in der Schlange gespeichert ist. Kopiert alle Elmente der Liste ab einem bestimmten Index in das übergebene Array. Entfernt das Element am Anfang der Schlange und liefert es zurück. Hängt ein Element an das Ende der Schlange an. Liefert eine Vorschau auf das erste Element der Schlange ohne es zu entfernen. Liefert ein Array mit den Elementen in der Schlange zurück. Verkürzt die Schlangenlänge auf die Zahl der aktuellen Einträge, falls die Schlange zu weniger als 90% voll ist. Fortsetzung auf der nächsten Seite Û 61 7.3 Queue<T> Methode Aufgabe Tabelle 7.3: Methoden und Properties von Queue<T> Das folgende Beispielprogramm legt eine neue Schlange für komplexe Zahlen an holt das vorderste Element der Schlange heraus und zeigt es an gibt eine Leerzeile aus gibt den Inhalt der restlichen Schlange aus using System ; using System . Collections . Generic ; class Program { static void Main ( string [] args ) { Queue < Complex > Schlange = new Queue < Complex >() ; Schlange . Enqueue ( new Complex (1 , 1) ) ; Schlange . Enqueue ( new Complex (2 , 2) ) ; Schlange . Enqueue ( new Complex (3 , 3) ) ; Console . WriteLine ( Schlange . Dequeue () ) ; Console . WriteLine () ; foreach ( Complex c in Schlange ) Console . WriteLine ( c ) ; } } class Complex { protected float re , im ; public Complex ( float re , float im ) { this . re = re ; this . im = im ; } public override string ToString () { return " ( " + re + " ," + im + " i ) " ; } } und es ergibt sich die Ausgabe (1 ,1 i ) (2 ,2 i ) (3 ,3 i ) 7 Collections 62 7.4 Stack<T> Die Klasse Stack<T> stellt einen First-In-Last-Out-Speicher oder Stapel dar. Das zuletzt zugefügte Element ist das erste Element, das den Stack verlässt. Methode void CopyTo(array, index) T Peek() T Pop() void Push(element) T[] ToArray() void TrimExcess() Aufgabe Kopiert alle Elmente des Stacks ab einem bestimmten Index in das übergebene Array. Liefert eine Vorschau auf das erste Element der Schlange ohne es zu entfernen. Entfernt das zuletzt gespeicherte Element und gibt es zurück. Speichert ein Element auf dem Stack. Liefert ein Array mit den Elementen im Stack zurück. Verkürzt die Schlangenlänge auf die Zahl der aktuellen Einträge, falls die Schlange zu weniger als 90% voll ist. Tabelle 7.4: Methoden und Properties von Stack<T> Das folgende Programm schreibt drei Werte auf den Stack zeigt sie an entnimmt einen Wert und zeigt ihn an zeigt die verbleibende Liste an using System ; using System . Collections . Generic ; class Program { static void Main ( string [] args ) { Stack < string > Stapel = new Stack < string >() ; Stapel . Push ( " Eins " ) ; Stapel . Push ( " Zwei " ) ; Stapel . Push ( " Drei " ) ; foreach ( string s in Stapel ) Console . WriteLine ( s ) ; Console . WriteLine ( " \ nEntnommen : " + Stapel . Pop () + " \ n " ) ; foreach ( string s in Stapel ) Console . WriteLine ( s ) ; } private static bool ContainsE ( string s ) { return s . Contains ( " e " ) ; } } und es ergibt sich die Ausgabe 63 7.5 Dictionary<TKey,TValue> Drei Zwei Eins Entnommen : Drei Zwei Eins 7.5 Dictionary<TKey,TValue> Ein Dictionary speichert Schlüssel-Werte-Paare und erlaubt assoziativen Zugriff auf die Werte über Schlüssel. Die generischen Typen beim Anlegen des Dictionaries geben Typ vom Schlüssel und Typ des Werts an. Die wichtigsten Properties und Methoden sind: Methode int Count T Item[schlüssel] Keys Values void Add(schlüssel, wert) void Clear() bool ContainsKey(schlüssel) bool ContainsValue(wert) bool Remove(schlüssel) bool TryGetValue(schlüssel, out wert) Aufgabe Liefert die Anzahl der Schlüssel-/Werte-Paare im Dictionary. Setzt und liest das Element mit dem angegebenen Schlüssel. Liefert eine Sammlung mit allen Schlüsseln. Liefert eine Sammlung mit allen Werten. Fügt den Wert unter dem angegebenen Schlüssel ein. Leert das Dictionary. Bestimmt, ob ein Schlüssel im Dictionary auftaucht. Bestimmt, ob ein Wert im Dictionary auftaucht. Entfernt das Element mit dem angegenen Schlüssel und meldet Erfolg. Liefert den Wert unter dem angegebenen Schlüssel und meldet Erfolg. Tabelle 7.5: Methoden und Properties von Dictionary<T> 7.6 SortedList Die sortierte Liste ist identisch zum Dictionary, nur dass die einzelnen Einträge im Dictionary nach dem Schlüssel geordnet sind. 7.7 foreach über eigene Sammlungen Zusätzlich zu den normalen Schleifen-Konstruktionen wie for und while besitzt C# auch noch die foreach-Schleife, die durch alle Elemente einer Sammlung iteriert. String [] getraenke = { " Bier " , " Milch " , " Wasser " }; foreach ( String g in getraenke ) { Console . WriteLine ( g ) ; } Damit derartige Iterationen auch über selbsterstellte Klassen möglich sind, stellt das .NET Framework 2.0 folgende Interfaces zur Verfügung: 64 7 Collections public interface IEnumerable < ItemType > { IEnumerator < ItemType > GetEnumerator () ; } public interface IEnumerator < ItemType > : IDisposable { ItemType Current { get ;} bool MoveNext () ; } Dabei gibt es keine Reset-Methode mehr wie noch im .NET Framework 1.0. Der Code für das Getränke-Beispiel sieht für generische Klassen folgendermaßen aus: public class CityCollection : IEnumerable < string > { string [] m_Cities = { " New York " ," Paris " ," London " }; public IEnumerator < string > GetEnumerator () { for ( int i = 0; i < m_Cities . Length ; i ++) yield return m_Cities [ i ]; } } 65 8 Delegates, Events und Exceptions Von C und C++ kennt die Programmiererin den Einsatz von Funktionszeigern, um sogenannte Callback-Funktionen einzurichten, die das Programm beim Eintreten eines bestimmten Ereignisses zurückruft. Das Windows-Betriebssystem arbeitet sehr viel mit Callback-Funktionen. Der CCompiler hat diesen Aufruf in den Aufruf einer Funktion an einer bestimmten Adresse im Speicher übersetzt. Dabei geriet der Rückgabewert der Funktion und deren Parameter in Vergessenheit. C# realisiert diese Aufrufe als Methodenaufrufe typ-sicher in Form von Delegates und deren Spezialform, den Events. 8.1 Delegates Meist verwenden Methoden übergebene Zahlenwerte oder Objekte als Parameter, um mit diesen zu arbeiten. In einigen Fällen soll eine Methode nicht einen Wert (Zahl oder Objekt) erhalten, sondern eine übergebene Methode aufrufen, die von Methodenaufruf zu Methodenaufruf unterschiedlich sein kann. Dies kann z.B. eine Rückruf-Funktion (Callback-Function) sein, die das Programm aufrufen soll, wenn ein Ereignis eintritt oder beispielsweise eine Vergleichsfunktion, die einer Sortier-Routine das Vergleichen abnimmt. Delegates dienen zum typ-sicheren Übergeben von Methoden an andere Methoden, indem sie einer Methoden-Signatur einen Namen geben. Ein Delegate Dazu muss zur Methoden-Signatur selbst noch ein verträglicher Rückgabewert dazukommen. gibt einer MethodenSignatur 8.1.1 Deklaration eines Delegates einen Namen Wie bei normalen Klassen auch unterscheidet man bei Delegates in die Deklaration und in die Instantiierung. Die Deklaration kann dort erfolgen, wo auch Klassendeklarationen erfolgen können. Bei der Deklaration erfährt der Compiler, welche Art von Methode das Delegate darstellt. Im folgenden Beispiel für die Deklaration eines Delegates erfährt der Compiler, dass das Delegate Methoden vertreten kann, die einen Integerwert als Rückgabewert haben und zwei Integerwerte als Parameter. Das so deklarierte Delegate kann nun als Stellvertreter für alle Methoden stehen, die 2 Integerparameter haben und einen Integerwert zurückliefern. Im Fall des Beispiels steht das Delegate einmal für die Addition und einmal für die Multiplikation: using System ; class Program { delegate int ZweiIntOP ( int x , int y ) ; static void Main ( string [] args ) { ZweiIntOP o ; o = new ZweiIntOP ( Addiere ) ; Console . WriteLine ( " Addition : " + o (4 , 2) ) ; o = new ZweiIntOP ( Multiplizere ) ; Console . WriteLine ( " Multiplikation : " + o (4 , 2) ) ; 8 Delegates, Events und Exceptions 66 } public static int Addiere ( int a , int b ) { return a + b ; } public static int Multiplizere ( int a , int b ) { return a * b ; } } ergibt folgende Ausgabe, die belegt, dass o(4,2) einmal wie ein Additions-Operator und einmal wie ein Multiplikations-Operator gewirkt hat: Addition : 6 Multiplikation : 8 Seit .NET 2.0 ist auch das direkte Zuweisen von Operationen ohne den new-Operator möglich: o = Addiere ; Console . WriteLine ( " Addition : " + o (4 , 2) ) ; o = Multiplizere ; Console . WriteLine ( " Multiplikation : " + o (4 , 2) ) ; Hinter der Und letztendlich brauchen die Methoden auch keine Namen mehr zu haben. Dann handelt es sich um anonyme Methoden. Das Schlüsselwort delegate kennzeichnet solche Methoden. anonymen Methode Da dies eigentlich ein normales Statement und kein Block ist, muss ein Semikolon folgen muss ein o = delegate ( int a , int b ) Semikolon { folgen return a + b ; }; Console . WriteLine ( " Addition : " + o (4 , 2) ) ; o = Multiplizere ; Console . WriteLine ( " Multiplikation : " + o (4 , 2) ) ; Das abschließende Beispiel verwendet eine Liste mit Methoden, um sie der Reihe nach auf zwei Zahlen anzuwenden. So könnten z.B. einfach Tabellen mehrerer mathematischer Funktionen entstehen: using System ; using System . Collections . Generic ; class Program { delegate int IntOP ( int x ) ; static void Main ( string [] args ) { List < IntOP > OpList = new List < IntOP >() ; OpList . Add ( Quadrat ) ; OpList . Add ( Kubik ) ; OpList . Add ( Quart ) ; Console . WriteLine ( " Zahl , Quadrat , Kubik , Quart " ) ; 67 8.1 Delegates for ( int i = 0; i <= 10; i ++) { Console . Write ( " \ n {0 ,4} ", i); foreach ( IntOP o in OpList ) Console . Write ( " {0 ,5} " , o ( i ) ) ; } } public static int Quadrat ( int a ) { return a * a ; } public static int Kubik ( int a ) { return a * a * a ; } public static int Quart ( int a ) { return a * a * a * a ; } } ergibt die Funktionstabelle Zahl , Quadrat , Kubik , Quart 0 1 2 3 4 5 6 7 8 9 10 0 1 4 9 16 25 36 49 64 81 100 0 1 8 27 64 125 216 343 512 729 1000 0 1 16 81 256 625 1296 2401 4096 6561 10000 8.1.2 Multicast-Delegates Bis hierher hat ein Delegate immer für eine einzige Method gestanden. Der Aufruf des Delegates entsprach dem Aufruf der gespeicherten Methoden. Der Aufruf mehrerer Methoden erfolgte durch das Aufrufen mehrerer Delegates. Ein Delegate kann jedoch auch mehr als eine Methode aufnehmen. Dann handelt es sich um ein Multicast-Delegate. Beim Aufruf eines Multicast-Delegates führt dieses jede darin gespeicherte Methode nacheinander aus. Da dann nicht klar ist, wessen Rückgabewert das Delegate dann zurückgeben soll, darf keine der Methoden einen Rückgabewert liefern. Der Compiler geht sogar automatisch davon aus, dass ein Delegate, das void-Methoden kapselt, ein Multicast-Delegate ist. Im folgenden Beispiel führt der Aufruf von p(i) zur Ausführung aller vier gespeicherten Funktionen. using System ; class Program MulticastDelegates kapseln voidMethoden 8 Delegates, Events und Exceptions 68 { delegate void VoidPrint ( int x ) ; static void Main ( string [] args ) { VoidPrint p ; p p p p = PrintZahl ; += PrintQuadrat ; += PrintKubik ; += PrintQuart ; Console . WriteLine ( " Zahl , Quadrat , Kubik , Quart " ) ; for ( int i = 0; i <= 10; i ++) { p(i); Console . WriteLine () ; } } public static void PrintZahl ( int a ) { Console . Write ( " {0 ,6} " , a ) ; } public static void PrintQuadrat ( int a ) { Console . Write ( " {0 ,6} " ,a * a ) ; } public static void PrintKubik ( int a ) { Console . Write ( " {0 ,6} " , a * a * a ) ; } public static void PrintQuart ( int a ) { Console . Write ( " {0 ,6} " , a * a * a * a ) ; } } 8.2 Events Windows-Applikationen sind nachrichten-basiert, d.h. die Applikation und Windows kommunizieren über Nachrichten in Schlangen. Bei den Entwurfsmustern der objektorienterten Programmierung warten z.T. Klassen darauf, über Änderungen von Fenstern und Forms informiert zu werden. Diese Information geschieht über Events. C# kapselt Events in Delegates und so haben wir bereits das Handwerkszeug gelernt, um Events zu verwenden. Das .NET Framework hat dafür bereits ein Delegate definiert, den EventHandler aus System. Dieser EventHandler kapselt Methoden, die keinen Rückgabewert liefern und zwei Parameter haben: das Objekt, das den Event auslöst und benutzerspezifische Daten. Namens-Konvention beim Programmieren von Eventhandlern ist object_event, wobei objekt das den Event auslösende Objekt ist und event das ausgelöste Ereignis, z.B. button1_press. 69 8.3 Exceptions Im folgenden Beispiel findet sich in der partiellen Klasse Form1 die Methode button_Click(object sender, EventArgs e), die eine MessageBox öffnet, in der die Bezeichnung des Buttons steht, der den Event gesendet hat. using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; namespace FormSamples { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; } private void Form1_Load ( object sender , EventArgs e ) { } private void button_Click ( object sender , EventArgs e ) { MessageBox . Show ((( Button ) sender ) . Name + " gedrückt " ) ; } } } Im zweiten Teil der partiellen Klasse sorgen this . button1 . Click += new System . EventHandler ( this . button_Click ) ; this . button2 . Click += new System . EventHandler ( this . button_Click ) ; dafür, dass das Laufzeitsystem den selbstgeschriebenen Eventhandler für beide Buttons aufruft. 8.3 Exceptions Fehler in Applikationen treten nicht nur aufgrund falscher Programmierung auf, sondern z.B. weil der Anwender inkonsistente Daten eingegeben hat oder weil eine Netzverbindung zusammengebrochen ist. Das Programm sollte diese Fälle vorhersehen können und entsprechend darauf reagieren. C# macht es der Programmiererin leicht, das Programm auf solche Fälle vorzubereiten: C# hat eine Reihe vordefinierter Exception-Klassen, die sich für Fehlerbehandlungen einsetzen lassen Programmiererinnen können eigene Exception-Klassen von der C# -eigenen ExceptionKlasse ableiten der try-catch-finally-Mechanismus sorgt für einen systematischen Umgang mit den Exceptions 8 Delegates, Events und Exceptions 70 Oft reicht das einfache Zurückgeben eines Fehlercodes nicht, um angemessen auf eine Ausnahmesituation zu reagieren. Möglicherweise steckt das Programm ja gerade in einem mehrfach verschachtelten Gebirge aus Methodenaufrufen, die nun alle auf den Fehlercode abprüfen und entsprechend darauf reagieren müssten. Exceptions in C# sind der einfachere Weg, gerade auch im Vergleich mit andern Programmiersprachen: nur die Methode, die die Exception wirft und die Methode, die sie fängt, müssen sich um die Exception kümmern. Die Grundlagen für das Exception-System bilden zwei Klassen aus dem Namespace System: SystemException Exceptions dieser oder davon abgeleiteter Klassen gehören zur Funktion des .NET-Runtime-Systems oder sind so allgemein, dass die Art von Ausnahmen in allen Klassen auftreten kann ApplicationException Diese Klasse bildet die Grundlage für benutzerdefinierte Exceptions. Eigene Klassen leiten von ihr ab zur Definition neuer und anwendungsspezifischer Ausnahmen. 8.3.1 Exceptions werfen Das C# -Schlüsselwort throw löst eine Exception aus: using System ; class Program { static void Main ( string [] args ) { int d = Convert . ToInt16 ( Console . ReadLine () ) ; int e = Convert . ToInt16 ( Console . ReadLine () ) ; Console . WriteLine ( teile (d , e ) ) ; } static int teile ( int dividend , int divisor ) { if ( divisor == 0) throw new DivideByZeroException () ; return dividend / divisor ; } } Hier reagiert das Programm mit einer Exception auf die Eingabe von 0 als Divisor: 4 0 Unhandled Exception : System . DivideByZeroException : Attempted to divide by zero . at Program . teile ( Int32 dividend , Int32 divisor ) in Program . cs : line 16 at Program . Main ( String [] args ) in Program . cs : line 10 8.3.2 Exceptions fangen Code, der mit Exceptions umgeht, teilt sich in drei Blöcke: 8.3 Exceptions 71 try Im try-Block steht der normale Programmcode, bei dessen Ausführung möglicherweise ein Fehler auftreten kann. catch Ein catch-Block behandelt eine Fehlersituation, die bei Ausführung des try-Blocks aufgetreten ist – falls sie aufgetreten ist. Mehrere catch-Blöcke fangen mehrere mögliche Exceptionarten ab. finally Den Code im finally-Block führt das Laufzeitsystem auf jeden Fall aus, entweder nach regulärem Durchlaufen des try-Blocks oder in Anschluss an die Ausnahmebehandlung im catch-Block. Ein finally-Block ist nicht nötig, falls das Laufzeitsystem keine “Aufräumarbeiten” zu erledigen hat. Im folgenden Bispiel fängt der erste catch-Block gezielt die Division durch Null ab. Der zweite catch-Block sammelt alle Exceptions ein, die im try-Block aufgetreten sind, wie z.B. die leere Eingabe, mit der Convert.ToInt16 nichts anfangen kann. static void Main ( string [] args ) { int d , e , f ; try { d = Convert . ToInt16 ( Console . ReadLine () ) ; e = Convert . ToInt16 ( Console . ReadLine () ) ; f = teile (d , e ) ; } catch ( DivideByZeroException ex ) { Console . WriteLine ( " DivideByZeroException aufgetreten ! " ) ; f = int . MaxValue ; } catch { Console . WriteLine ( " Andere Exception aufgetreten ! " ) ; } Console . WriteLine ( f ) ; } Exceptions, für die kein catch-Block existiert, fängt das Laufzeitsystem ab. 8.3.3 Eigene Exceptions Eigene Exceptions leiten sich von der Klasse System.Exception ab und können einen selbstdefinierten Text mit sich tragen, der die Ursache für die Ausnahme weiter detailliert. using System ; class Program { public enum getraenk : byte { Bier , Wein , Milch , Met , Wasser , Absynth } static void Main ( string [] args ) { string [] gs = Enum . GetNames ( typeof ( getraenk ) ) ; 8 Delegates, Events und Exceptions 72 foreach ( String s in gs ) trinke ( s ) ; } static void trinke ( string getraenk ) { if ( getraenk != " Wasser " ) Console . WriteLine ( " Trinke " + getraenk ) ; else throw new UndrinkableException ( getraenk ) ; } } class UndrinkableException : ApplicationException { public UndrinkableException ( string getraenk ) : base ( " Untrinkbar : " + getraenk ) { } } 8.3.4 Verschachtelte Exceptions Oft kann ein catch-Block auf eine Exception nicht abschließend reagieren, hat aber neue Erkenntnisse, die er an die endgültige Fehlermeldung anhängen kann. Ohne Verlust von Exception-Details funktioniert das mit “inneren Exceptions”. Der Konstruktor der System.Exception-Klasse ist dazu überladen mit einer Version mit zwei Argumenten, dem Exception-Text und einer Exception, die von weiter unten aus der Verschachtelungshierarchie kommt. using System ; class Program { static void Main ( string [] args ) { DoOuter () ; } public static void DoOuter () { try { DoInner () ; } catch ( Exception e ) { throw new OuterException ( " in Catch ! " , e ) ; } } public static void DoInner () { throw new InnerException ( " von Hand " ) ; } } 8.3 Exceptions 73 class OuterException : ApplicationException { public OuterException ( string grund , Exception e ) : base ( "O - Exception : " + grund , e ) { } } class InnerException : ApplicationException { public InnerException ( string grund ) : base ( "I - Exception : " + grund ) { } } ergibt folgende Ausgabe: Unhandled Exception : OuterException : O - Exception : in Catch ! ---> InnerException : I - Exception : von Hand at Program . DoInner () in Program . cs : line 24 at Program . DoOuter () in Program . cs : line 14 --- End of inner exception stack trace --at Program . DoOuter () in Program . cs : line 18 at Program . Main ( String [] args ) in Program . cs : line 7 Nun könnte das Programm auch die Exceptions selbst abfangen, statt das dem Laufzeitsystem zu überlassen: static void Main ( string [] args ) { try { DoOuter () ; } catch ( Exception e ) { while ( e != null ) { Console . WriteLine ( " Uuups : " + e . Message ) ; e = e . InnerException ; } } } und das Ergebnis zeigt, dass das Programm alle Exceptions erkannt hat und gegebenenfalls auf jede reagieren könnte: Uuups : O - Exception : in Catch ! Uuups : I - Exception : von Hand 74 9 Threads Bis hierher starteten alle Programme bei der Main-Methode und es gab einen einzigen Ausführungs-Pfad durch das Programm. Ein Thread ist ein unabhängiger Ausführungs-Pfad in einem Programm. Threads sorgen beispielsweise dafür, dass ein Programm auf Benutzereingaben reagieren kann, während es eine Datei lädt oder eine längerdauernde Berechnung anstellt. Das .NET-Laufzeitsystem kann mehrere unabhängige Ausführungs-Pfade (Threads) gleichzeitig verwalten. Den ersten Ausführungspfad startet das Laufzeitsystem bei Programmstart, indem es ihn mit der Main()-Methode anfangen lässt. Methode Start() Name Suspend() Resume() Abort() Join() CurrentThread Sleep(zeit) Priority Aufgabe Startet die Ausfühung des Threads. Name des Threads. Für’s Debugging empfiehlt sich hier ein verständlicher Name. Hält den Thread zeitweilig an. Setzt die Ausführung eines angehaltenen Threads fort. Bricht die Ausführung eines Threads komplett ab. Das Laufzeitsystem wirft dazu eine Ausnahme vom Typ ThreadAbortException, die keine Methode abfängt - gibt so aber den entsprechenden finallyBlöcken Gelegenheit, ihren Nachlass zu ordnen. Wartet, bis ein mit Abort() abgebrochener Thread wirklich zu Ende ist. Statische Property, die das Thread-Objekt des gerade laufenden Threads zurückliefert. Statische Methode, die den aktuellen Thread für eine bestimmte Zeit (in Millisekunden) blockiert. Property die die Priorität des Threads angibt oder verändert: Highest, AboveNormal, Normal, BelowNormal und Lowest. Tabelle 9.1: Die wichtigsten Methoden und Properties von Thread 9.1 Threads erzeugen und verwalten Die Programmiererin erzeugt einen neuen Thread, indem sie ein neues Objekt der Klasse Thread aus dem Namespace System.Threading anlegt. Damit der Thread an der richtigen Stelle mit der Ausführung anfängt, erhält er als Konstruktor-Parameter den Startpunkt mitgegeben. Der Startpunkt ist ein Delegate vom Typ ThreadStart ohne Parameter und mit einem leeren Rückgabewert. Das nachfolgende Beispielprogramm legt erst einen Einsprungpunkt fest, dann erzeugt es den eigentlichen Thread, gibt ihm einen verständlichen Namen und startet dann den Thread: using System ; using System . Threading ; class Program { static void Main ( string [] args ) { 9.1 Threads erzeugen und verwalten 75 // Einsprungpunkt festlegen ThreadStart SchleifenThreadStart = new ThreadStart ( Schleife ) ; // Thread erzeugen Thread SchleifenThread = new Thread ( SchleifenThreadStart ) ; // Menschenlesbarer Name SchleifenThread . Name = " Schleifen - Thread " ; // Thread starten SchleifenThread . Start () ; } static void Schleife () { for ( int i = 0; i < 1000; i ++) Console . WriteLine (i , i * 2 * Math . PI / 360) ; } } Unter Verwendung anonymer Methoden für das Delegate ThreadStart: using System ; using System . Threading ; class Program { static void Main ( string [] args ) { // Thread erzeugen Thread SchleifenThread = new Thread ( delegate () { for ( int i = 0; i < 1000; i ++) Console . WriteLine ( " {0 ,3}:{1} " , i , Math . Sin ( i * 2 * Math . PI / 360) ) ; }) ; // Menschenlesbarer Name SchleifenThread . Name = " Schleifen - Thread " ; // Thread starten SchleifenThread . Start () ; } } Das folgende Beispielprogramm startet in einem zweiten Thread eine Schleife, die im Sekundentakt von 10 aus rückwärts bis 0 zählt, bricht diese Schleife nach 5 Sekunden ab und wartet, bis der abgebrochene Thread fertig ist, um dann eine Programm-Ende-Meldung abzugeben: using System ; using System . Threading ; class Program { static void Main ( string [] args ) { 9 Threads 76 // Einsprung angeben ThreadStart ts = new ThreadStart ( CountDown ) ; // Thread erzeugen Thread CountDownThread = new Thread ( ts ) ; // Menschenlesbarer Name CountDownThread . Name = " CountDown - Thread " ; // Thread starten CountDownThread . Start () ; // 5 Sekunden warten Thread . Sleep (5000) ; // Andern Thread abbrechen CountDownThread . Abort () ; // Auf Abbruch warten CountDownThread . Join () ; Console . WriteLine ( " Programm - Ende " ) ; } static void CountDown () { for ( int i = 10; i >= 0; i - -) { Console . WriteLine ( " {0 ,2} " , i ) ; Thread . Sleep (1000) ; } } } 9.2 Prioritäten von Threads Oft sollen Programme im Hintergrund Aufgaben niedriger Priorität durchführen, währen z.B. das Programm auf Eingaben der Anwenderin wartet. Wenn die Anwenderin dann eine Eingabe macht, soll das Programm diese mit hoher Priorität abarbeiten. C# kennt den Aufzählungstyp ThreadPriority, um einzelnen Threads ihre Wichtigkeit zuweisen zu können: Highest, AboveNormal, Normal, BelowNormal und Lowest. 9.3 Synchronisation von Threads Greifen mehrere Threads auf Variable zu, hängt es oft vom zufälligen Timing ab, welcher Thread die Variable verändert. So könnten z.B. zwei Threads beide i = i + 1 rechnen und es könnte - bei einem ursprünglichen Wert von i folgendes passieren: 1. der erste Thread liest i und erhält 3 2. ein Threadwechsel passiert und der zweite Thread setzt fort 3. der zweite Thread liest i und erhält 3 9.3 Synchronisation von Threads 77 4. der zweite Thread schreibt 4 5. ein Threadwechsel passiert und der erste Thread setzt fort 6. der erste Thread schreibt 4 So wird aus der Variablen i mit dem Startwert von 3 nach 2 Additionen von 1 der Wert 4! Je länger eine Operation auf einer Variablen dauert, um so größer ist die Gefahr eines gleichzeitigen Zugriffs auf die Variable von einem andern Thread aus. Vor allem große Collections sind hier sehr gefährdet. Das folgende Beispielprogramm schreibt einen Wert in ein größeres Array und schaut direkt danach nach, ob der Wert im Array noch drin steht. Bei längeren Schleifendurchläufen kommt es durchaus zu Schreib-/Lese-Kollisionen. using System ; using System . Threading ; class Program { static int [] ia = new int [10000]; static void Main ( string [] args ) { ThreadStart ts = new ThreadStart ( Konkurrenz ) ; Thread KonkurrenzThread = new Thread ( ts ) ; KonkurrenzThread . Name = " Konkurrenz - Thread " ; KonkurrenzThread . Start () ; for ( int i = 1000; i < 2000; i ++) if (! FuelleTeste (1) ) Console . WriteLine ( " Clash in Main bei " + i ) ; } static void Konkurrenz () { for ( int i = 2000; i < 3000; i ++) if (! FuelleTeste (2) ) Console . WriteLine ( " Clash in Konkurrenz bei " + i ) ; } static bool FuelleTeste ( int wert ) { bool ret = true ; // optimistisch // Füllen for ( int i = 0; i < ia . Length ; i ++) ia [ i ] = wert ; // Testen for ( int i = 0; i < ia . Length ; i ++) { if ( ia [ i ] != wert ) { ret = false ; break ; } Zugriff von Threads auf Collections sicher regeln! 9 Threads 78 } return ret ; } } In C# gibt es mit dem lock-Mechanismus ein einfaches Mittel, um Zugriffe auf Variable zu synchronisieren. Ein mit lock geklammerter Block erlaubt innerhalb des Blocks exklusiven Zugriff auf eine Variable. Das Laufzeitsystem suspendiert andere Threads, die versuchen, gleichzeitig auf die Variable zuzugreifen. lock ( variable ) { Statement ( s ) ; } Mit diese kleinen Änderung läuft das Programm ohne Konflikte beim Zugriff auf das Array: static bool FuelleTeste ( int wert ) { bool ret = true ; // optimistisch // das neue lock - Konstrukt lock ( ia ) { // Füllen for ( int i = 0; i < ia . Length ; i ++) ia [ i ] = wert ; // Testen for ( int i = 0; i < ia . Length ; i ++) { if ( ia [ i ] != wert ) { ret = false ; break ; } } } return ret ; } 79 10 Dateien Alle wichtigen Klassen des Dateisystems befinden sich im Namespace System.IO. 10.1 Die Laufwerke Die DriveInfo-Klasse von C# informiert über die an einem System angeschlossenen Laufwerke. Sie hat eine relevante statische Methode, GetDrives(), die ein Array von DriveInfo-Objekten zurückliefert. Zu jedem dieser DriveInfo-Objekte existieren eine Reihe von Properties, die über das einzelne Laufwerk informieren: Property AvailableFreeSpace DriveFormat DriveType IsReady Name RootDirectory TotalFreeSpace TotalSize VolumeLabel Aufgabe Gibt den freien Speicher für den aktuellen Benutzer an. Liefert das Format des Dateisystems, z.B. NTFS oder FAT32. Liefert den Laufwerkstyp: Fest, Entfernbar, CD-ROM, etc. Ist das Laufwerk bereit? Liefert den Namen bzw. Laufwerksbuchstaben. Gibt das Root-Verzeichnis des Laufwerks zurück. Gibt den freien Speicher für alle Benutzer an. Gibt die Laufwerkskapazität zurück. Liest oder schreibt den Laufwerksbezeichnung. Tabelle 10.1: Die Properties von DriveInfo Vor dem Zugriff auf die Laufwerkseigenschaften sollte das Programm mit IsReady sicherstellen, dass das Laufwerk überhaupt die gewünschten Daten liefern kann. using System ; using System . IO ; class Program { static void Main ( string [] args ) { DriveInfo [] dis = DriveInfo . GetDrives () ; foreach ( DriveInfo di in dis ) { if ( di . IsReady ) { Console . WriteLine ( " ===== Laufwerk " + di . Name + " ( " + di . DriveFormat + " , " + di . DriveType + " ) " ) ; Console . WriteLine ( " Name : " + di . VolumeLabel ) ; Console . WriteLine ( " Speicher : " + di . AvailableFreeSpace + " von " + di . TotalSize ) ; } } } } 10 Dateien 80 10.2 Verzeichnisse und Dateien Zum Zugriff auf Verzeichnisse und Dateien kennt C# zwei verschiedene Klassen: Die Klassen Directory und File, die nur statische Methoden enthalten. Deren Aufruf mit Pfad oder Filesystem-Objekt führt dazu, dass sie jedesmal die benötigte Information neu aus dem Dateisystem einlesen. Dafür muss das Programm nicht erst eine Klasse des Dateisystems instantiieren. Die Klassen DirectoryInfo und FileInfo, die bei mehrfacher Verwendung effektiver sind, weil sie sich die Informationen aus dem Dateisytem merken. Methode Attributes CreationTime Exists Extension FullName LastAccessTime LastWriteTime Name Parent Root Create() CreateSubdirectory() Delete() GetAccessControl() GetDirectories() GetFiles() MoveTo() Refresh() SetAccessControl() Aufgabe Setzt oder löscht die Attribute. Liest oder setzt das Erzeugungsdatum. Prüft, ob das Objekt existiert. Liefert die Namenserweiterung. Liefert den kompletten Pfadnamen zurück. Datum des letzten Zugriffs. Datum des letzten Schreibzugriffs. Name des Objekts. Mutterverzeichnis. Wurzelverzeichnis des Pfades. Verzeichnis erstellen. Unterverzeichnis erstellen, auch relativ zum aktuellen DirectoryObjekt. Löscht ein Verzeichnis. Liefert die Zugriffsrechte. Liste der Unterverzeichnisse. Liste der Dateien im aktuellen Verzeichnis. Verschiebt das ganze Verzeichnis an einen andern Ort. Aktualisiert die Verzeichnis-Informationen. Ändert die Zugriffsrechte. Tabelle 10.2: Die Properties und Methoden von DirectoryInfo Das folgende Programm gibt, beginnend beim Home-Verzeichnis der Userin, alle Dateien und Verzeichnisse aus und geht dabei durch alle Unterverzeichnisse: using System ; using System . IO ; class Program { static void Main ( string [] args ) { string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; RecurseDirs ( Home , 0) ; } static void RecurseDirs ( string dir , int depth ) { DirectoryInfo di = new DirectoryInfo ( dir ) ; 10.2 Verzeichnisse und Dateien 81 DirectoryInfo [] dirs = di . GetDirectories () ; String indentication = new String ( ’ ’, depth ) ; foreach ( DirectoryInfo d in dirs ) { Console . WriteLine ( indentication + d ) ; RecurseDirs ( d . FullName , depth +2) ; } FileInfo [] files = di . GetFiles () ; Console . ForegroundColor = ( ConsoleColor ) Enum . Parse ( typeof ( ConsoleColor ) , " DarkGray " ) ; foreach ( FileInfo d in files ) { Console . WriteLine ( indentication + d ) ; } Console . ForegroundColor = ( ConsoleColor ) Enum . Parse ( typeof ( ConsoleColor ) , " White " ) ; } } Methode Attributes CreationTime Directory DirectoryName Stringdarstellung des vollen Directory-Pfads. Exists Extension FullName IsReadOnly LastAccessTime LastWriteTime Length Name CopyTo() Create() Delete() GetAccessControl() MoveTo() Refresh() Replace() SetAccessControl() Aufgabe Setzt oder löscht die Attribute. Liest oder setzt das Erzeugungsdatum. Das dazugehörige Directory-Object. Prüft, ob das Objekt existiert. Liefert die Namenserweiterung. Liefert den kompletten Pfadnamen zurück. Liest oder setzt die Schreibbererechtigung auf “nur Lesen”. Datum des letzten Zugriffs. Datum des letzten Schreibzugriffs. Dateigröße. Name des Objekts. Kopiert die Datei in eine neue Datei. Datei erstellen. Löscht eine Datei. Liefert die Zugriffsrechte. Verschiebt die Datei an einen andern Ort. Aktualisiert die Verzeichnis-Informationen. Überschreibt die Datei durch die aktuelle. Ändert die Zugriffsrechte. Tabelle 10.3: Einige Properties und Methoden von FileInfo 10.2.1 Kopieren, Verschieben, Löschen Ein Programm verschiebt Dateien und Verzeichnisse mit der MoveTo()-Methode der FileInfooder FolderInfo-Klasse und löscht sie mit deren Delete()-Methode. Dateien kopieren geht 10 Dateien 82 mir der CopyTo()-Methode, jedoch gibt es keine Methode zum Kopieren ganzer Verzeichnisse. using System ; using System . IO ; class Program { static void Main ( string [] args ) { string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; FileInfo tf = new FileInfo ( Home + @ " \ test . txt " ) ; FileInfo of = new FileInfo ( Home + @ " \ test2 . txt " ) ; if ( of . Exists ) of . Delete () ; tf . CopyTo ( Home + @ " \ test2 . txt " ) ; } } 10.2.2 Dateien lesen und schreiben Die File-Klasse bietet schon einige statische Methoden zum Lesen und Schreiben von Dateien: Methode OpenRead() OpenText() OpenWrite() ReadAllBytes() ReadAllLines() ReadAllText() WriteAllBytes() WriteAllLines() WriteAllText() Aufgabe Öffnet eine Datei zum Lesen. Öffnet eine existierende UTF-8-Datei zum Lesen. Öffnet eine existierende Datei zum Schreiben. Liest eine binäre Datei in ein Byte-Array. Liest alle Zeilen zeilenweise in ein String-Array. List eine ganze Datei in einen String. Schreibt das Byte-Array in eine binäre Datei. Schreibt ein String-Array als Zeilen in eine Datei. Schreibt einen String in eine Datei. Tabelle 10.4: Einige Methoden der File-Klasse using System ; using System . IO ; using System . Text ; class Program { static void Main ( string [] args ) { int x , y ; StringBuilder EinMalEins = new StringBuilder () ; for ( y = 1; y <= 10; y ++) { for ( x = 1; x <= 10; x ++) EinMalEins . Append ( String . Format ( " {0 ,3} " , x * y ) ) ; EinMalEins . Append ( Environment . NewLine ) ; } Console . WriteLine ( EinMalEins ) ; 10.3 Streams 83 string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; File . WriteAllText ( Home + @ " \ test . txt " , EinMalEins . ToString () ) ; } } Environment.NewLine ist der vom Betriebssystem unabhängige Zeilenvorschub. 10.3 Streams Streams sind ein Schritt zur einheitlichen Behandlung von Daten aus Dateien, über Netze übertragene Daten, Interprozess-Strömen (Pipes) und Shared Memory. Die beiden wichtigsten Klassen von C# sind: FileStream zum Lesen und Schreiben von binären Daten in binäre Files. StreamReader und StreamWriter zum Lesen und Schreiben von Textfiles. 10.3.1 Lesen und Schreiben von Textfiles StreamReader beinhaltet die Methode Readline und und StreamWriter das entsprechende WriteLine. Diese Methoden unterstützen die Formatierung wie bei Format und verhalten sich so wie Console.WriteLine. using System ; using System . IO ; using System . Text ; class Program { static void Main ( string [] args ) { string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; StreamReader sr = new StreamReader ( Home + @ " \ test . txt " ) ; string line ; int linecount = 0; while (( line = sr . ReadLine () ) != null ) { Console . WriteLine ( " {0 ,5} {1} " ,++ linecount , line ) ; } } } 10.3.2 Lesen und Schreiben von Binärdateien BinaryReader beinhaltet Methoden zum Lesen aller Arten von Typen. Die am häufigsten verwendeten sind ReadByte() und ReadChar(). 84 10 Dateien FileStream fs = new FileStream ( " input . bin " , FileMode . Open , FileAccess . Read ) ; BinaryReader br = new BinaryReader ( fs ) ; for ( int c = 0; c < fi . Length ; c ++) DoSomethingWith ( br . ReadByte () ) ; 10.3.3 XML-Serialisierung Platform# unabhängiges C kann Klasseninstanzen und deren Inhalte als XML-Strom schreiben und lesen. So kann ein Programm z.B. Datenmengen in einem platformunabhängigen Format schreiben und lesen. Schreiben von Daten auf Datei schreiben: ProgrammEinstellungen XmlSerializer serializer = new XmlSerializer ( typeof ( UserData ) ) ; FileStream fs = new FileStream ( " userdata . xml " , FileMode . Create ) ; serializer . Serialize ( fs , this . userData ) ; fs . Close () ; Daten von Datei lesen: XmlSerializer serializer = new XmlSerializer ( typeof ( UserData ) ) ; FileStream fs = new FileStream ( " userdata . xml " , FileMode . Open ) ; this . userData = ( UserData ) serializer . Deserialize ( fs ) ; fs . Close () ; Dabei kann der Serializer zur Laufzeit auf alle öffentlichen Felder und Eigenschaften zugreifen. Speziell der Zugriff auf Eigenschaften ermöglicht die Beibehaltung der Kapselung interner Information (”Black-Box-Prinzip”). 85 11 Forms und Controls 11.1 Label Ein Label ist ein Textfeld zur Erläuterung von Eingabefeldern oder zur Ausgabe dynamischer Daten. Beim Ändern seiner Text-Property ändert sich auch der Text des Labels. 11.2 MessageBox Eines der wichtigsten Werkzeuge zur Kommunikation zwischen Programm und Anwenderin ist die MessageBox. Von der einfachen Ausgabe eines Textes bis zur komplexen Box mit mehreren Knöpfen stehen alle Box-Arten zur Verfügung. Box ist die statische Klasse System.Windows.Forms.MessageBox des .NET-Laufzeitsystems. Außer dem ersten Parameter, dem Nachrichtentext, sind alle weitere Parameter optional. MessageBox . Show ( NachrichtenText , Fenster Überschrift , Knöpfe , Icon ) ; Die Auswahl der Knöpfe ist recht reichhaltig und verwirrend, wobei, je nach Sprachversion von Windows, die Knopf-Beschriftungen in Landessprache erfolgen: Element Bedeutung AbortRetryIgnore Abort-, Retry- und Ignore-Knopf. OK OK-Knopf. OKCancel OK- und Cancel-Knopf. RetryCancel Retry- und Cancel-Knopf. YesNo Yes- und No-Knopf. YesNoCancel Yes-, No- und Cancel-Knopf. Tabelle 11.1: Knöpfe der MessageBoxButtons-Enumeration Auch stehen eine Element Asterisk Error Exclamation Hand Information None Question Stop Reihe von Hinweis- und Warnsymbolen Bedeutung Kleines i im Kreis. Weisses X im roten Kreis. Ausrufezeichen in gelbem Dreieck. Weisses X im roten Kreis (!). Kleines i im Kreis. Kein Symbol. Fragezeichen im Kreis. Weisses X im roten Kreis (!). zur Verfügung: Fortsetzung auf der nächsten Seite Û 11 Forms und Controls 86 Element Warning Bedeutung Ausrufezeichen in gelbem Dreieck. Tabelle 11.2: Symbole der MessageBoxIcons-Enumeration Ein komplexerer Aufruf der MessageBox sieht etwa folgendermaßen aus: using System ; using System . Collections . Generic ; using System . Windows . Forms ; namespace ControlDemo { static class Program { static void Main () { MessageBox . Show ( " Es gibt viel zu wenig verschiedene Icons " , " Icon - Armut " , MessageBoxButtons . YesNo , MessageBoxIcon . Question ) ; } } } und führt zu folgender Anzeige: 11.3 Button Button ist ein einfacher Knopf, dessen Parameter die Programmiererin im Properties-Feld einstellen kann. Interessant ist hier vor allem die Zuordnung unter dem Reiter Events zum Click-Eventhandler des Knopfes, den das Laufzeitsystem aufruft, wenn die Benutzerin den Knopf drückt. using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; namespace ControlDemo { public partial class Form1 : Form 11.4 CheckBox { public Form1 () { InitializeComponent () ; } private void buttonClick ( object sender , EventArgs e ) { Button btn = sender as Button ; MessageBox . Show ( " Knopf " + btn . Name + " gedrückt " ) ; } } } 11.4 CheckBox CheckBox ist das Feld zum Ankreuzen, das bei C# drei Zustände annehmen kann: Checked Angekreuzt: Das Feld hat ein Häkchen. Unchecked Leer: Das Feld hat kein Häkchen. Indeterminate Unbestimmt: Das Feld ist grau hinterlegt. Der folgende Programmausschnitt zeigt den Zustand von 3 unabhängigen Checkboxen an: private void buttonClick ( object sender , EventArgs e ) { String msg = " Box 1: " + checkBox1 . CheckState + " \ n " + " Box 2: " + checkBox2 . CheckState + " \ n " + " Box 3: " + checkBox3 . CheckState ; MessageBox . Show ( msg ) ; } und liefert folgendes Ergebnis: 87 88 11 Forms und Controls 11.5 RadioButton Der RadioButton sieht in einer seiner Darstellungsarten aus wie die CheckBox, aber mit runden Feldern, die sich gegenseitig auslösen. Die zweite Darstellungsform sieht aus wie normale Buttons, aber auch diese lösen sich gegenseitig aus: private void buttonClick ( object sender , EventArgs e ) { String msg = " Button 1: " + radioButton1 . Checked + " \ n " + " Button 2: " + radioButton2 . Checked + " \ n " + " Button 3: " + radioButton3 . Checked ; MessageBox . Show ( msg ) ; } Einfacher als jedesmal das Drücken des “Auslesen”-Knopfes abzuwarten geht es, indem das Programm einen Handler für den CheckedChange-Event der RadioButton-Objekte implementiert. Im folgenden Beispiel laufen alle Radiobuttons auf den gleichen Event-Handler, der den Namen und den Zustand des Radio-Buttons ausgibt. Diesen Handler ruft das Laufzeitsystem gleich zu Programmstart auf, weil der erste der RadioButtons automatisch selektiert wird. Bei jedem Knopfdruck durch die Bedienerin geht ein Event für das Abwählen des “alten” Knopfes an den Eventhandler und ein Event für das Selektieren des “neuen” Knopfes. private void r adi oBu tto nCh ec ked Cha nge d ( object sender , EventArgs e ) { RadioButton btn = sender as RadioButton ; MessageBox . Show ( btn . Text + " ist " + btn . Checked ) ; } 11.6 ComboBox, ListBox und CheckedListBox 89 11.6 ComboBox, ListBox und CheckedListBox 11.6.1 ComboBox Die ComboBox ist geeignet für die Auswahl von genau einem Element aus einer Liste, die ListBox für die Auswahl von einem oder mehreren Elementen und die CheckedListBox darüberhinaus zum Ankreuzen der gewünschten Einträge. Im folgenden Beispiel füllt der Eventhandler für RadioButtons, je nach Wahl des RadioButtons, eine ComboBox mit einer Liste aus Farben oder einer Liste aus Formen und wählt das erste Element vor: using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; namespace ControlDemo { public partial class Form1 : Form { private static string [] farbenListe = { " rot " , " orange " , " gelb " , " grün " , " blau " , " violett " }; private static string [] formenListe = { " Punkt " , " Linie " , " Dreieck " , " Parallelogramm " , " Rechteck " , " Quadrat " , " Ellipse " , " Kreis " }; public Form1 () { InitializeComponent () ; } private void r adi oBu tto nCh eck edC han ge d ( object sender , EventArgs e ) { RadioButton btn = sender as RadioButton ; comboBox1 . Items . Clear () ; if ( btn . Name == " farben " && btn . Checked ) { foreach ( String f in farbenListe ) comboBox1 . Items . Add ( f ) ; comboBox1 . SelectedIndex = 0; } else if ( btn . Name == " formen " && btn . Checked ) { foreach ( String f in formenListe ) comboBox1 . Items . Add ( f ) ; comboBox1 . SelectedIndex = 0; } } } } 11 Forms und Controls 90 11.6.2 ListBox Die ListBox lässt die Auswahl von einem oder mehreren Elementen zu, je nach Parameter für SelectionMode: None Keine Auswahl möglich. One Auswahl von nur einem Element möglich. Die ListBox verhält sich hierbei wie eine ComboBox, nur dass mehr Einträge gleichzeitig sichtbar sein können, wenn das Anzeigefeld groß genug ist. MultiSimple Mehrfachanwahl ist möglich. MultiExtended Mehrfachauswahl ist möglich. Darüberhinaus ist auch die Anwahl mit Shift, CTRL und den Pfeiltasten möglich. Im folgenden Beispiel füllt der Eventhandler für RadioButtons, je nach Wahl des RadioButtons, eine ListBox mit einer Liste aus Farben oder einer Liste aus Formen: private void r adi oBu tto nCh ec ked Cha nge d ( object sender , EventArgs e ) { RadioButton btn = sender as RadioButton ; listBox1 . Items . Clear () ; if ( btn . Name == " farben " && btn . Checked ) { foreach ( String f in farbenListe ) listBox1 . Items . Add ( f ) ; } else if ( btn . Name == " formen " && btn . Checked ) { foreach ( String f in formenListe ) listBox1 . Items . Add ( f ) ; } } 11.7 ListView 91 11.6.3 CheckedListBox Die CheckedListBox erweitert die ListBox um Kästchen zum Ankreuzen bei ansonstem gleichen Verhalten. 11.7 ListView Ein ListView kann Daten auf vier verschiedene Arten darstellen: 1. als Text mit einem optionalen großen Icon, 2. als Text mit einem optinalen kleinen Icon, 3. oder als Text oder kleine Icons in vertikalen Listen, 4. oder als Detailansicht mit mehreren Spalten. Der Datei-Explorer vewendet z.B. einen ListView zur Anzeige. Die einzelnen Einträge in ListView leiten sich von ListViewItem ab und besitzen eine TextProperty, die den anzuzeigenden Text enthält, und eine SubItems-Property, die die Texte für die Detailansicht enthält. Die Detailansicht benötigt darüberhinaus noch die Überschriften über die einzelnen Spalten mittels myListView.Columns.Add(). Die folgenden Programm-Ausschnitte leiten eine Klasse FileItem von ListViewItem ab und setzen im Konstruktor den Namen für die Anzeige sowie Dateigröße und letztes Zugriffsdatum für die Detailansicht: 11 Forms und Controls 92 using using using using System ; System . Collections . Generic ; System . Text ; System . IO ; namespace ControlDemo { class FileItem : System . Windows . Forms . ListViewItem { private FileInfo fileInfo ; public FileItem ( FileInfo fi ) { fileInfo = fi ; base . Text = fileInfo . Name ; base . SubItems . Add ( fileInfo . Length . ToString () ) ; base . SubItems . Add ( fileInfo . LastWriteTime . ToShortDateString () ) ; } } } Der Initializer der Form füllt den ListView mit einer Liste der Dateien im Heimatverzeichnis und setzt die Spaltenüberschriften für die Detailansicht. Ein Klick auf die RadioButtons schaltet die Ansicht des Listviews um. using using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; System . IO ; namespace ControlDemo { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; DirectoryInfo di = new DirectoryInfo ( Home ) ; FileInfo [] files = di . GetFiles () ; foreach ( FileInfo f in files ) listView1 . Items . Add ( new FileItem ( f ) ) ; listView1 . Columns . Add ( " Dateiname " ) ; listView1 . Columns . Add ( " Größe " ) ; listView1 . Columns . Add ( " Modifikation " ) ; } 93 11.8 TextBox private void r adi oBu tto nCh eck ed Cha nge d ( object sender , EventArgs e ) { RadioButton btn = sender as RadioButton ; if ( btn . Checked ) { switch ( btn . Name ) { case " LargeIcon " : listView1 . View = View . LargeIcon ; break ; case " SmallIcon " : listView1 . View = View . SmallIcon ; break ; case " List " : listView1 . View = View . List ; break ; case " Details " : listView1 . View = View . Details ; break ; default : MessageBox . Show ( " Unbekannter Radiobutton " ) ; break ; } } } } } 11.8 TextBox Die TextBox ist das wohl verbreiteste Controlelement. Sie stellt einen String oder ein Array aus Strings zeilenweise dar. Einige der wichtigeren Eigenschaften der TextBox sind: Eigenschaft AcceptsReturn BackColor BorderStyle Aufgabe Schaltet um zwischen RETURN innerhalb der TextBox und dem Aktivieren des Default-Knopfes. Hintergrundfarbe der Box. Rahmenart der Box. Fortsetzung auf der nächsten Seite Û 11 Forms und Controls 94 Eigenschaft Enabled Focused Font Height Lines Location MaxLength Multiline Name PasswordChar ReadOnly ScrollBars SelectedText SelectionLength SelectionStart Size TabIndex TabStop Text TextLength Visible Width Funktion An und Abschalten der Eingabemöglichkeit. Liefert zurück, ob die Box den Focus hat. Setzt oder liest die aktuelle Font in der Box. Setzt oder liest die Box-Höhe. Liest oder schreibt die Textzeilen in der Box mittels eines StringArrays. Gibt den Punkt der linken oberen Ecke im einschließenden Container an. Maximale Zeichenzahl die die Benutzerin eintippen darf. Nimmt die Box eine oder mehrere Zeilen auf. Lesen und Schreiben des Box-Namens. Setzt oder liest das Zeichen, das Passworte in einer einzeiligen Box verdeckt. Auslesen oder Ändern des “nur-Lesen”-Status der Box. Liest oder setzt die Rollbalken: Horizontal, Vertikal, Beide oder keinen. Liest den selektierten Text aus oder ersetzt ihn durch einen neuen Text. Liest oder setzt die Zahl der selektierten Buchstaben. Setzt den Index des ersten selektierten Buchstabens. Gibt die Breite und Höhe der Box an. Liest oder schreibt die Tab-Reihenfolge der Box. Bestimmt, ob Tab überhaupt zur Box führt. Liest oder schreibt den Text in der Box. Liest die Länge des Textes in der Box. Liest und ändert die Sichtbarkeit der Box. Liest und schreibt die Breite der Box. Tabelle 11.3: Wichtige Eigenschaften der TextBox Der folgende Beispielcode erzeugt auf einem Formular, das nur das Label mit der Beschriftung “TextBox-Beispiel” enthält, eine Textbox per Programm: using using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; System . IO ; namespace ControlDemo { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; InitializeComponent () ; // Textbox - Demo für die C # Vorlesung TextBox textBox = new TextBox () ; textBox . Location = new Point (13 , 30) ; 11.8 TextBox 95 textBox . Name = " DemoBox " ; textBox . Size = new Size (100 , 20) ; textBox . TabIndex = 2; this . Controls . Add ( textBox ) ; } } } SelectionStart legt den Anfang einer Ausfahl fest, SelectionLength die Länge einer Auswahl. Der folgende Programmabschnitt sucht auf Knopfdruck den String TextBox im eingegebenen Text, wählt ihn aus und macht ihn mit Focus sichtbar: private void button1_Click ( object sender , EventArgs e ) { String searchText = " TextBox " ; int selStart = textBox1 . Text . IndexOf ( searchText ) ; textBox1 . SelectionStart = selStart ; textBox1 . SelectionLength = searchText . Length ; textBox1 . Focus () ; } 11.8.1 ErrorProvider Der ErrorProvider ist keine Control sondern eine Komponente und erscheint so im Designer nicht auf der Form-Oberfläche sondern in der Sammlung in der Fußzeile. Pro Formular ist nur ein ErrorProvider nötig. Der ErrorProvider besitzt eine Methode SetError(), die die Control anwählt, neben der das Fehlersymbol erscheinen soll und einen Hilfetext, den das Laufzeitsystem beim Überfahren mit der Maus ausgibt. Im folgenden Beispiel implementiert die TextBox einen Event-Handler für jede Änderung des Textes in der Box und registriert ihn beim Laufzeitsystem mit folgendem Code: this . textBox1 . TextChanged += new System . EventHandler ( this . textBox1TextChanged ) ; 11 Forms und Controls 96 Der Eventhandler prüft nach jeder Änderung über einen regulären Ausdruck, ob in der Box genau 5 Zahlen stehen und setzt die Fehlermeldung auf leer, wenn die Eingabe der PLZ korrekt ist: using using using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; System . IO ; System . Text . RegularExpressions ; namespace ControlDemo { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; } public void textBox1TextChanged ( object sender , EventArgs e ) { TextBox tb = sender as TextBox ; Regex rexp = new Regex ( @ " \ d {5} " ) ; if ( rexp . IsMatch ( tb . Text ) ) errorProvider1 . SetError ( tb , " " ) ; else errorProvider1 . SetError ( tb , " PLZ besteht aus 5 Zahlen " ) ; } } } 11.8.2 Eingaben Abfangen und Verändern Die TextBox erzeugt mit jedem Tastendruck KeyPress-Events, die auch eine Möglichkeit zur Vermeidung von Fehleingaben bieten: Ein eigener KeyPress-Handler kann Buchstaben verändern oder filtern und selbst eigene oder andere Zeichen an den Eingabetext anhängen. Dieser Hander hat ein von EventArgs abgeleitetes zweites Methodenargument, das das gedrückte Zeichen enthält. Der Handler signalisiert durch Setzen von e.Handled, dass sich das Laufzeitsystem nicht weiter um die Eingabe zu kümmern braucht. using System ; using System . Collections . Generic ; using System . ComponentModel ; 11.8 TextBox using using using using using using 97 System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; System . IO ; System . Text . RegularExpressions ; namespace ControlDemo { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; helpProvider1 . SetHelpString ( textBox1 , " Postleitzahl : 5 Ziffern " ) ; } public void textBox1KeyPress ( object sender , KeyPressEventArgs e ) { TextBox tb = sender as TextBox ; String c = e . KeyChar . ToString () ; if ( " 0123456789 " . Contains ( c ) ) if ( tb . Text . Length < 5) tb . AppendText ( c ) ; e . Handled = true ; } } } 11.8.3 HelpProvider Der HelpProvider ist eine Komponente, wie der ErrorProvider. Zum Anschalten eines Hilfetextes erhält der HelpProvider zwei Argumente: Die Referenz einer Control, zu der der Hilfetext gehört. Den Hilfetext, der erläutert, welche Eingabe die Control erwartet. Wenn sich nun der Cursor im Eingabefeld befindet und die Anwenderin F1 drückt, erscheint der angegebene Hilfstext. Der ErrorProvider kann noch viel mehr: Er lässt sich mit einer Hilfedatei verknüpfen und springt innerhalb dieser ein wählbares Stichwort an, wenn die Anwenderin F1 drückt. using using using using using using using using using System ; System . Collections . Generic ; System . ComponentModel ; System . Data ; System . Drawing ; System . Text ; System . Windows . Forms ; System . IO ; System . Text . RegularExpressions ; 11 Forms und Controls 98 namespace ControlDemo { public partial class Form1 : Form { public Form1 () { InitializeComponent () ; helpProvider1 . SetHelpString ( textBox1 , " Postleitzahl : 5 Ziffern " ) ; } public void textBox1TextChanged ( object sender , EventArgs e ) { TextBox tb = sender as TextBox ; Regex rexp = new Regex ( @ " \ d {5} " ) ; if ( rexp . IsMatch ( tb . Text ) ) errorProvider1 . SetError ( tb , " " ) ; else errorProvider1 . SetError ( tb , " PLZ besteht aus 5 Zahlen " ) ; } } } 11.9 RichTextBox Die RichTextBox unterstützt das Rich Text Format (RTF) und bietet weitere Möglichkeiten zum Formatieren von Texten. Sie bietet unter anderem Änderungen des Zeichensatzes über SelectFont. Farbänderungem mit SelectColor. Listen durch SelectBullet. Alle diese Änderungen wirken auf wie bei der TextBox selektierte Textbereiche. private void button1_Click ( object sender , EventArgs e ) { String searchText = " RichTextBox " ; int selStart = richTextBox1 . Text . IndexOf ( searchText ) ; richTextBox1 . SelectionStart = selStart ; richTextBox1 . SelectionLength = searchText . Length ; richTextBox1 . SelectionColor = Color . Blue ; } 11.10 ProgressBar 99 11.10 ProgressBar Der ProgressBar ist eine intuitive Anzeige, wieviel Zeit etc. von einer Tätigkeit schon vergangen ist und wie lange es wohl noch dauern kann. Der Progressbar ist einfach einzustellen und bereichert die meisten Anwendungen um eine Rückmeldung, dass grade etwas vor sich geht und vieviel schon abgearbeitet ist. Minimum Unterer Wert des Balkens. Maximum Oberer Wert des Balkens. Step Schrittweite, in der PerformStep() weiterschaltet. Value Liest oder setzt den aktuellen Wert des Balkens. PerformStep() Schaltet einen Step weiter. pf . progressBar1 . Minimum = 0; pf . progressBar1 . Maximum = 100; pf . progressBar1 . Value = 0; pf . progressBar1 . Step = 10; 11.11 StatusStrip Ein StatusStrip nimmt unter anderem die wichtigsten Elemente zum Anzeigen von Fortschritt auf, den ProgressBar und ein StatusLabel. Die Textmeldung im StatusLabel informiert darüber, was das Programm gerade tut, der Fortschrittsbalken im ProgressBar darüber, wie weit das Programm gerade damit ist. Das einfachste Vorgehen: StatusStrip aus der Auswahlleiste in die aktuelle Form einfügen. 11 Forms und Controls 100 StatusLabel im StatusStrip anwählen. ProgressBar im StatusStrip anwählen. Elemente rechts neben dem StatusLabel verschieben sich zur Laufzeit mit der Länge des Textes im StatusLabel 11.12 ContextMenuStrip Ein ContextMenuStrip kann zu jeder Control ein Context-Menu anbieten, das die rechte Maustaste aufruft. Dazu den ContextMenuStrip aus der Auswahlbox in die Form ziehen und innerhalb der Control deren ContextMenuStrip-Eigenschaft auf den Namen des ContextMenuStrip setzen.g 11.13 OpenFileDialog Der OpenFileDialog öffnet eine eigenständige Dialogbox, in der die Anwenderin zur Laufzeit die Datei, die das Programm einlesen soll, angeben kann. Eine Reihe von Eigenschaften ermöglichen eine einfache Konfiguration des Dialogs: Eigenschaft AddExtension CheckFileExists CheckPathExists DefaultExt FileName FileNames Filter Aufgabe Soll die Dialogbox automatisch Dateinamenserweiterungen anfügen? Überprüfung, ob die angebebene Datei existiert. Überprüfung, ob der angegebene Pfad existiert. Setzt oder liest die voreingestellte Dateinamenserweiterung. Setzt und vor allem liest den gewählten Dateinamen aus. Liest alle Dateinamen bei Mehrfachselektion aus. Liest oder setzt den aktuellen Dateinamenfilter-String: z.B. "Text files (*.txt)|*.txt|All files (*.*)|*.*" oder "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*". FilterIndex InitialDirectory Multiselect Title Liest oder schreibt den derzeit aktuellen Filterwert. Verzeichnis, in dem die OpenFileDialog-Box mit der Suche beginnt. Sind Mehrfachauswahlen zulässig? Überchrift der Dialogbox. Tabelle 11.4: Wichtige Eigenschaften von OpenFileDialog Der Programmabschnitt zum Anzeigen einer Dialogbox für eine zu lesende .tex-Datei sieht damit folgendermaßen aus: 11.14 Eigenständige Forms 101 private void button1_Click ( object sender , EventArgs e ) { openFileDialog1 . Title = " C # Vorlesung Demo " ; openFileDialog1 . Filter = " Tex - Dateien (*. tex ) |*. tex " ; openFileDialog1 . FileName = " " ; openFileDialog1 . InitialDirectory = @ " C :\ Documents and Settings \ Admin \ My Documents \ HS - Esslingen \ Vorlesungen \ C # " ; DialogResult result = openFileDialog1 . ShowDialog () ; if ( result == DialogResult . OK ) { tbFile . Text = openFileDialog1 . FileName ; } } Die Dialoge zum Speichern einer Datei (SaveFileDialog) und eines Ordners (FolderBrowserDialog) verhalten sich ganz zur Auswahl entsprechend. 11.14 Eigenständige Forms Eigenständige Forms lassen sich als weitere Forms in Projekte einbinden und mit new instantiieren. Danach ist der normale Zugriff auf öffentliche Methoden, Properties oder Variable möglich. So ist beispielsweise ein Popup-Fenster mit einem Progress-Bar einfach zu realisieren. Der folgende Programmabschnitt öffnet auf Knopfdruck eine neue Form: private void button1_Click ( object sender , EventArgs e ) { pf = new ProgressForm () ; pf . progressBar1 . Minimum = 0; pf . progressBar1 . Maximum = 100; pf . progressBar1 . Value = 0; pf . progressBar1 . Step = 10; pf . Show () ; } 102 11 Forms und Controls 11.15 Panels Das Panel ist eine Control, die andere Controls beinhaltet. Das kann das Programm nutzen, indem es beispielsweise alle Controls innerhalb eines Panels abschalten kann, indem es das Panel abschaltet. Ebenfalls kann das Programm bei Platzmangel den Panel-Rand auf AutoScroll schalten und die Benutzerin kann durch alle Controls durchscrollen. Die Sonderformen FlowLayoutPanel und TableLayoutPanel ermöglichen das Layout von Controls, das sich besser an Größenveränderungen der umgebenden Form anpasst. 103 12 Tipps und Tricks 12.1 Dokumentierende Kommentare Mit C# hat ein neuer Stil von Kommentaren Einzug gehalten, der das automatische Erzeugen von Code- und Klassendokumentation unterstützt. Diese Kommentare beginnen mit drei Schrägstrichen (///). Damit sind sie rückwärtskompatibel zu bisherigen Kommentaren mit zwei Schrägstrichen und wohl trotzdem nicht allzu oft zufällig zu finden. Beispiel: // / < summary > // / The constructor gets the server to connect to // / and the user credentials to log in // / </ summary > // / < param name =" servername " > The Server to connect to // / A < see cref =" System . String "/ > // / </ param > // / < param name =" username " > User login name // / A < see cref =" System . String "/ > // / </ param > // / < param name =" password " > Password to log in // / A < see cref =" System . String "/ > // / </ param > public Jabber ( string servername , string username , string password ) { this . servername = servername ; this . username = username ; this . password = password ; contacts = new Hashtable () ; } Es gibt zwei Arten von XML-Tags in Dokumentations-Kommentaren: Die primären Tags, die eine Gruppe von Tags einleiten, und die sekundären Tags, die Dokumentations-Texte verändern. 12.1.1 Primäre Tags Die primären Tags sind <remarks>, <summary>, <example>, <exception>, <param>, <value>, <permission>, <returns>, <seealso> und <includes>. <remarks> Beschreibt einen Typ, wie z.B. eine Klasse oder eine Struktur. Microsoft Visual Studio und MonoDevelop fügen beide bei Eingabe von drei Kommentarstrichen über einer Klasse fälschlicherweise das <summary>-Tag ein. // / < remarks > // / Class to handle all Jabber specific issues . // / </ remarks > public class Jabber { 12 Tipps und Tricks 104 private string servername ; private string username ; <summary> Für die Beschreibung von Elementen innerhalb von Typen, also Methoden, Eigenschaften und Felder. Zur Beschreibung von Typen nicht empfohlen aber oft verwendet, weil die Entwicklungsumgebungen dieses Tag automatisch erzeugen. // / < summary > // / The constructor gets the server to connect to // / and the user credentials to log in // / </ summary > public Jabber ( string servername , string username , string password ) { this . servername = servername ; this . username = username ; this . password = password ; <example> Beispiel für einen Einsatz der Klasse oder Methode: // / < example > // / < code > // / Jabber j = new Jabber (" xmpp . hs - esslingen . de " , " melcher " , " passwd ") ; // / </ code > // / </ example > <exception> Beschreibt die Ausnahmen, die die Methode oder Klasse möglicherweise wirft. Attribut-Parameter cref für dieses Tag ist die Klasse der Ausnahme, die existieren muss, damit das Dokumentationsprogramm automatisch darauf verweisen kann. // / < exception cref =" JabberException " > // / Thrown , when the constructor can ’ t connect to the specified Jabber server // / </ exception > <param> Gibt Details zu Methoden-Parametern an. Der Attribut-Parameter name gibt dabei den Parameternamen an: // / < param name =" servername " > The Server to connect to // / </ param > <value> Gibt Details zum Eigenschaften-Parameter an: // / < value > // / The Length property contains number of Servers in the Serverlist // / </ value > 105 12.1 Dokumentierende Kommentare <permission> Wie andere Programmteile auf einen Typ oder eine Methode, Eigenschaft, Feld zugegreifen dürfen, gibt das <permission>-Tag an. // / < permission cref =" System . Security . PermissionSet " > Public // / Access </ permission > <returns> Dieses Tag beschreibt den Rückgabewert von Methoden oder Eigenschaften: // / < returns > Success or failure of login procedure // / A < see cref =" System . Boolean "/ > // / </ returns > <seealso/> Ein alleinstehendes Tag, das auf ein artverwandtes Thema verweist. Der Dokumentationscompiler verwandelt das cref-Attibut in einen Link. // / < seealso cref =" Server "/ > <include/> Falls Dokumentation und Code in getrennten Dateien untergebracht sind, weist das <include/>Tag den Dokumentationscompiler an, die angegebene Datei einzulesen und die Dokumentatinskommentare so zu verarbeiten, als stünden sie in der aktuellen Datei. // / < include file = ’ MyCommentFile . xml ’ // / path = ’ doc / exmpl [ @name =" M : ExampleClass "]/* ’/ > 12.1.2 Sekundäre Tags Sekundäre Tags bestimmen die Formatierung von Elementen innerhalb der primären Tags. Das aufwändigste sekundäre Tag ist die Liste <list>: // / // / // / // / // / // / // / // / // / // / // / // / < list type =" table " > < listheader > < term > Overview </ term > < description > Function </ description > </ listheader > < item > < term > Types </ term > < description > Generate type list </ description > </ item > < item > < term > Members </ term > < description > Generate member list </ description > </ item > < item > < term > Type </ term > < description > Generate doc for a single type </ description > </ item > </ list > Tag <c> <code> Funktion Schliesst eine Code-Zeile ein. Verwendet für Inline-Code. Schliesst mehrere Code-Zeilen ein. Fortsetzung auf der nächsten Seite Û 12 Tipps und Tricks 106 Tag <list> <listhead> <item> <term> <description> <para> <paramref> <see> Funktion Definiert eine formatierte Liste. Gibt die Überschrift der formatierten Liste an. Ein einzelner Eintrag der Liste. Gibt das Thema des Listeneintrags an. Gibt die Beschreibung zum Thema (s.o.) an. Erzeugt einen Paragrafen-Umbruch. Bewirkt spezielle Formatierung eines Parameters. Erzeugt einen Hyperlink auf ein anderes existierendes Symbol. Tabelle 12.1: Sekundäre Dokumetations-Tags 12.2 Laufzeitmessung Die Laufzeit eines Programms lässt sich am einfachsten mit den eingebauten DateTime-Objekten und den TimeSpan-Objekten bewerkstelligen: // ... // Zeitmessung in die Wege leiten DateTime tStart = DateTime . Now ; // // Hier der zu messende Programmabschnitt // // ... // Zeitmessung ausgeben DateTime tEnde = DateTime . Now ; TimeSpan tDiff = new TimeSpan ( tEnde . Ticks - tStart . Ticks ) ; Console . WriteLine ( " Rechenzeit : " + tDiff . ToString () ) ; 12.3 Zufallszahlen Die Random-Klasse unterstützt eine Reihe von Methoden zum Erzeugen von Zufallzahlen. Nach Instantiieren eines Objekts (mit einer eigenen Zufallszahlenfolge) liefert die Methode Next(limit) die nächste Zufallszahl im Bereich von 0 bis kleiner Limit: Random rnd = new Random () ; // ... int zZahl = rnd . Next ( max +1) ; 12.4 Besondere Verzeichnisse Das .NET-Laufzeitsystem kennt eine Reihe von außergewöhnlichen Verzeichnissen, so z.B. das Heimat-Verzeichnis der Benutzerin. Das Heimatverzeichnis ist in der MyDocuments-Property der SpecialFolder-Klasse im Namespace Environment. string Home = Environment . GetFolderPath ( Environment . SpecialFolder . MyDocuments ) ; 107 Index Symbols LinkedList . . . . . . . . . . . . . . . . . . . . . . . . . . 58 for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 foreach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 A G anonyme Methode . . . . . . . . . . . . . . . . . . . . 66 as-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Ausdruck regulärer . . . . . . . . . . . . . . . . . . . . . . . . . 50 Garbage Collector . . . . . . . . . . . . . . . . . . . . . 20 generische Methode . . . . . . . . . . . . . . . . . . . 35 B Bedingung if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 switch . . . . . . . . . . . . . . . . . . . . . . . . . . 11 break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 C Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Copy-Kontruktor . . . . . . . . . . . . . . . . . . . . . . 19 D delegate . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Delegates Multicast . . . . . . . . . . . . . . . . . . . . . . . . 67 Destruktor . . . . . . . . . . . . . . . . . . . . . . . . 20, 21 Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 E Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . 31 Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 Exception eigene . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 innere . . . . . . . . . . . . . . . . . . . . . . . . . . . .72 Expression regular . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Extension-Methoden . . . . . . . . . . . . . . . . . . 30 F FiFo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .60 Finalize() . . . . . . . . . . . . . . . . . . . . . . . . . . 20 I cdif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Initialisierung von Variablen . . . . . . . . . . . . . . . . . . . . . 7 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .37 ICloneable . . . . . . . . . . . . . . . . . . . . . 38 ICollection . . . . . . . . . . . . . . . . . . . . 40 IComparable . . . . . . . . . . . . . . . . . . . . 38 IConvertible . . . . . . . . . . . . . . . . . . . 38 IEnumerable . . . . . . . . . . . . . . . . 37, 39 IEnumerator . . . . . . . . . . . . . . . . . . . . 37 IFormatable . . . . . . . . . . . . . . . . . . . . 38 IList . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 is-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . 25 K Klasse sealed . . . . . . . . . . . . . . . . . . . . . . . . . . 22 Klassen, partielle . . . . . . . . . . . . . . . . . . . . . . 23 Konstruktor Copy- . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 statischer . . . . . . . . . . . . . . . . . . . . . . . . 18 Kontruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 L Linked List . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Liste verlinkt . . . . . . . . . . . . . . . . . . . . . . . . . . 58 M Main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 Methode Index 108 anonyme . . . . . . . . . . . . . . . . . . . . . . . . . 66 generische . . . . . . . . . . . . . . . . . . . . . . . .35 mit variabler Parameterzahl . . . . . . . 28 sealed . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Methoden überschreiben Extension Methoden . . . . . . . . . . . . . . 30 virtual . . . . . . . . . . . . . . . . . . . . . . . . . 29 Multicast Delegates . . . . . . . . . . . . . . . . . . . 67 O öffentlich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Operator as . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 is . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 P Parameter variable Anzahl . . . . . . . . . . . . . . . . . . . 28 partielle Klassen . . . . . . . . . . . . . . . . . . . . . . 23 Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 public . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Q struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 cdswitch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Synchronisation von Threads . . . . . . . . . . 76 T Thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 abbrechen . . . . . . . . . . . . . . . . . . . . . . . . 75 Synchronisation . . . . . . . . . . . . . . . . . . 76 Threads erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . 74 throw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 txt:delegate . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 U unmanaged Ressouce . . . . . . . . . . . . . . . . . . 20 using . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 V Variable statische . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Variablen-Initialisierung . . . . . . . . . . . . . . . . . 7 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 R Referenztyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 regulärer Ausdruck . . . . . . . . . . . . . . . . . . . . 50 Ressource unmanaged . . . . . . . . . . . . . . . . . . . . . . 20 S Sammlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Schleife break . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 continue . . . . . . . . . . . . . . . . . . . . . . . . 15 foreach . . . . . . . . . . . . . . . . . . . . . . . . . 15 for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 while . . . . . . . . . . . . . . . . . . . . . . . . 13, 14 sealed Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 Methode . . . . . . . . . . . . . . . . . . . . . . . . . 27 Signatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 SortedList . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 Statischer Kontruktor . . . . . . . . . . . . . . . . . 18 Statische Variable . . . . . . . . . . . . . . . . . . . . . . 9 StringBuilder . . . . . . . . . . . . . . . . . . . . . . 53 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 W Wertetyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 while . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13, 14