UNIVERSITY OF APPLIED SCIENCES Grosses Seminar WS 04/05 Prof. Dr. Bachmann Thema: C# anhand von ECMA-334 von Daniel Wiese Matr.Nr. 701930 07.12.2004 Inhaltsverzeichnis 1. Einleitung ............................................................................................................................ 3 2. Die Geschichte von C# .................................................................................................... 3 3. Standard ECMA – 334 .................................................................................................... 3 4. Entwicklungsumgebung .................................................................................................. 4 5. Sprachelemente von C# ................................................................................................... 4 5.1 Das erste Programm ......................................................................................................... 4 5.2 Datentypen ....................................................................................................................... 5 5.2.1 Typumwandlung........................................................................................................ 6 5.2.2 Arrays ........................................................................................................................ 7 5.2.3 Vereinheitlichung der Datentypen ............................................................................ 7 5.3 Variablen und Parameter.................................................................................................. 8 5.4 Automatische Speicherverwaltung................................................................................... 9 5.5 Ausdrücke....................................................................................................................... 10 5.6 Anweisungen (Statements)............................................................................................. 11 5.7 Klassen ........................................................................................................................... 11 5.7.1 Konstanten (constants) ............................................................................................ 12 5.7.2 Felder (fields) .......................................................................................................... 12 5.7.3 Methoden (methods) ............................................................................................... 12 5.7.4 Eigenschaften (properties)....................................................................................... 13 5.7.5 Ereignisse (events) .................................................................................................. 14 5.7.6 Operatoren (operators) ............................................................................................ 14 5.7.7 Indizierer (indexers) ................................................................................................ 16 5.7.8 Instanzkonstruktoren (instance constructors).......................................................... 16 5.7.9 Destruktoren (destructors)....................................................................................... 17 5.7.10 Statische Konstruktoren (static constructors)........................................................ 17 5.7.11 Vererbung (inheritance) ........................................................................................ 18 5.8 Strukturen (structs)......................................................................................................... 18 5.9 Schnittstellen (interfaces)............................................................................................... 19 5.10 Delegaten (delegate)..................................................................................................... 20 5.11 Enumerationen (enums) ............................................................................................... 20 5.12 Namespaces und Assemblies ....................................................................................... 21 5.13 Versioning .................................................................................................................... 22 5.14 Attribute (attributes)..................................................................................................... 22 6. Quellen .............................................................................................................................. 23 1. Einleitung Das vorliegende Dokument soll eine knappe Übersicht über die Programmiersprache C# , so wie sie in dem Standard der Europäischen Standardisierungsbehörde ECMA festgelegt wird, darstellen. Der Autor setzt voraus, dass die Leser dieser Ausarbeitung bereits Erfahrung mit anderen Programmiersprachen gesammelt haben. Insbesondere wären Kenntnisse in Java- und C/C++ Programmierung vom Vorteil, da die hier vorgestellte Sprache C# weitgehend auf C/C++ basiert und auch viele Sprachelemente aus Java übernommen hat. Die Gliederung der Ausarbeitung entspricht weitgehend der Struktur des ECMA-334 Standards, sodass es möglich sein sollte die entsprechenden Passagen im Standard zu verfolgen. Es soll gezeigt werden, dass mit etwas Erfahrung im Programmieren auch das Erlernen anderer Sprachen anhand von offiziellen Standards möglich ist. 2. Die Geschichte von C# Mitte der 90-er begann der große Siegeszug der plattformunabhängigen Programmiersprache Java. Als Microsoft daraufhin versuchte, sich Marktanteile davon zu sichern und mit der Weiterentwicklung der Umgebung begann, wurde dieses Vorhaben durch ein Gerichtsbeschluss von der Firma Sun gestoppt. Deshalb wurde von Microsoft beschlossen, eine eigene Umgebung von Null an zu entwickeln. Die Sprache C# (sprich „see-sharp“ oder auch „cis“ im deutschen Sprachraum) wurde von der Firma Microsoft entwickelt und wurde Ende 2000 zusammen mit dem .NET-Framework („dot-net“ ausgesprochen), einer neuen innovativen Programmier- und Laufzeitumgebung, als direkter Konkurrent zu Sun’s Java in einer Vorabversion veröffentlicht. C# sollte dabei die Vorzeigesprache der Umgebung werden. Wie der Name schon sagt basiert C# zum größten Teil auf den bewährten Sprachen C und C++ aber auch viele Elemente aus Java wurden übernommen. Im Sommer Jahr 2000 wurde C# von Microsoft bei der Europäischen Standardisierungsorganisation ECMA (European Computer Manufacturers Association) zur Standardisierung eingereicht. Ein Jahr später veröffentlichte die ECMA den Standard ECMA-334 „C# Language Specification“, welcher auch 2003 als Grundlage für den ISO Standard (ISO/IEC 23270) diente. 3. Standard ECMA – 334 Der Standard ECMA-334 „C# Language Specification“ ist mittlerweile in der zweiten Edition frei auf der Seite von der Europäischen Standardisierungsorganisation verfügbar (http://www.ecma-international.org/publications/standards/ecma-334.htm). Der Standard spezifiziert die Form und legt die Interpretation der Programme fest, die in der Programmiersprache C # geschrieben werden. Er legt fest: • Die Struktur der C# Programme • Die Syntax und die Einschränkungen der Sprache C# • Semantische Regeln zur Interpretation der C# Programme • Einschränkungen und Grenzen der konformen Implementierung Er legt nicht fest: • Mechanismen für die Implementierung der C# Programme für die datenverarbeitende Systeme • • • • • Mechanismen für die Ausführung der C# Anwendungen Mechanismen zur Umwandlung der Eingabe-Daten für die Nutzung von den C# Anwendungen Mechanismen zur Umwandlung der Ausgabe-Daten nach der Erzeugung von den C# Anwendungen Den Umfang oder Komplexität eines Programms und seiner Daten, welches die Kapazität der verarbeitenden Systeme übersteigen kann Alle minimalen Anforderungen an ein System, um die konforme Implementierung zu unterstützen. 4. Entwicklungsumgebung Für die Entwicklung der Anwendungen in C# bietet Microsoft ihr Produkt „Visual Studio .NET“ als Entwicklungsumgebung. Diese ist recht umfangreich und sicherlich sehr verbreitet. Es gibt aber auch sehr gute kostenlose Alternativen, denn das .NET Framework selbst ist frei verfügbar und darf kostenlos genutzt werden. Für die Entwicklung eigener Projekte werden mehrere Komponenten benötigt: • Das .NET – Framework von Microsoft für Windows (http://www.microsoft.com/downloads/details.aspx?displaylang=de&FamilyID=262d 25e3-f589-4842-8157-034d1e7cf3a3) • Das .NET – Framework SDK ebenfalls von Microsoft (http://www.microsoft.com/downloads/details.aspx?familyid=9B3A2CA6-3647-40709F41-A333C6B9181D&displaylang=de) • Ein Texteditor zum Erstellen von Programmen Für Unix und Windows gibt es ebenfalls eine freie .Net - Implementierung namens Mono (http://www.mono-project.com), welcher aber noch ohne Forms (GUI) auskommen muss, und deshalb erst mal nur GTK+ Komponenten verwendet. Zum Schreiben von Quelltexten reicht natürlich auch ein einfacher Texteditor, Entwicklungsumgebungen bieten jedoch eine komfortablere Arbeit, schon allein deswegen, weil der Compiler nicht jedes Mal per Hand auf der Konsole aufgerufen werden muss. Darüber hinaus bieten sie „Code Competition“ und bessere Projektverwaltung. Eine sehr solide Umgebung ist Borland C# Builder (http://www.borland.com/products/downloads/download_csharpbuilder.html) und ist für den Privatgebrauch kostenlos. Eine weitere Alternative ist das Open-Source Projekt SharpDevelop (http://www.sharpdevelop.net/OpenSource/SD), das ständig weiterentwickelt wird. 5. Sprachelemente von C# 5.1 Das erste Programm Als erstes Beispiel eines einfachen C# Programms wird das schon mittlerweile fast zum Standard gewordene „Hallo Welt!“. using System; class Hello { static void Main() { Console.WriteLine("Hallo Welt!"); } } Wie man schnell sieht, ist C# wie Java rein objektorientiert, auch die Struktur der Anweisungen entspricht der aus Java bekannten Notation. Dementsprechend gibt es keine global definierten Methoden oder Variablen. Der Quellcode wird typischerweise in einer Textdatei mit Endung .cs abgespeichert und wird z.B. beim .NET C# Compiler mit dem Befehl csc hello.cs kompiliert, die erstellte Anwendung erzeugt die Ausgabe „Hallo Welt!“ auf der Konsole. Der Dateiname muss übrigens nicht wie in Java mit dem Klassennamen identisch sein. Der Startpunkt jeder C# Anwendung ist immer die statische (static) Methode Main. Alle benötigten Standard - Klassenbibliotheken sind in CLI (Common Language Infrastructure) enthalten. Der Compiler erzeugt ein Zwischencode in MSIL (Microsoft Intermediate Language), der danach vom Just-In-Time Compiler teilweise in Maschinencode übersetzt wird. Um namespaces nutzen zu können und so schnell die Referenzierung der Elemente durchführen zu können, benutz man das Schlüsselwort using. In dem obigen Beispiel würde die Anweisung ohne der Verwendung von using folgendermaßen aussehen: System.Console.WriteLine(„Hallo Welt!“); (Vgl. in C++: std::cout ohne using namespace std;) 5.2 Datentypen In C# gibt es zwei Arten von Datentypen: Wertetypen (value types) und Verweistypen (reference types). Wertetypen beinhalten einfache Typen wie z.B. char, int, double, enum Typen und structs. Verweistypen sind Klassen, Interfaces, Delegaten und Arrays. Die vordefinierten, nativen Datentypen sollten den meisten aus C/C++ bekannt sein. Bei den Referenztypen sind nur zwei vordefiniert: object und String. Object ist der Basistyp aller anderen Typen – insofern werden „primitive“ Typen wie z.B. int auch als Objekte behandelt. Der String-Typ repräsentiert den Wert eines Unicode Zeichenstrings. Den Unterschied zwischen den Werte- und Verweistypen kann man im folgenden Programm verdeutlichen: using System; class Class1 { public int Value = 0; } class Test { static void Main() { int val1 = 0; int val2 = val1; val2 = 123; //<- nur val2 wird verändert Class1 ref1 = new Class1(); Class1 ref2 = ref1; ref2.Value = 123; //<- auch ref1 betroffen Console.WriteLine("Values: {0}, {1}", val1, val2); Console.WriteLine("Refs: {0}, {1}", ref1.Value, ref2.Value); } } Die Ausgabe: Values: 0, 123 Refs: 123, 123 Während es bei Wertetypen - Variablen nur eigene Werte verändert werden, führt eine Änderung der Verweistyp – Variable auch zur Aktualisierung der referenzierten Variable. An dem Beispiel sieht man auch wie die Ausgabe der eigentlichen Werte erfolgt. In der folgenden Tabelle sind alle vordefinierten Datentypen aufgelistet. Quelle: ECMA-334 5.2.1 Typumwandlung Die Umwandlung der Typen geschieht wie in C/C++ entweder implizit oder explizit, wobei man immer beachten muss, dass beim expliziten Umwandeln es leicht zum Datenverlust kommen kann. Die nachfolgenden Beispiele verdeutlichen es: using System; class Test { static void Main() { int intValue = 123; long longValue = intValue; Console.WriteLine("{0}, {1}", intValue, longValue); } } Ausgabe: 123,123 -> implizite Konvertierung von int nach long stellt kein Problem dar. Im Gegensatz dazu ein Beispiel mit expliziter Umwandlung: using System; class Test { static void Main() { long longValue = Int64.MaxValue; int intValue = (int) longValue; Console.WriteLine("(int) {0} = {1}", longValue, intValue); } } Ausgabe: (int) 9223372036854775807 = -1 Der Grund dafür ist klar – ein Überlauf. Der cast - Operator darf sowohl bei impliziter als auch expliziter Umwandlung verwendet werden. 5.2.2 Arrays Arrays können in C# ein- und mehrdimensional sein, „Rechteckige“ und verzweigte Arrays werden unterstützt. Eine kurze Übersicht der möglichen Array-Ausdrücke: class Test { static void Main() { int[] a1 = new int[] {1, 2, 3}; int[,] a2 = new int[,] {{1, 2, 3}, {4, 5, 6}}; int[,,] a3 = new int[10, 20, 30]; int[][] j2[0] = j2[1] = j2[2] = //eindimensional, int //zweidimensional, int //dreidimensional, int j2 = new int[3][]; //array von (array von int) new int[] {1, 2, 3}; new int[] {1, 2, 3, 4, 5, 6}; new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9}; } } 5.2.3 Vereinheitlichung der Datentypen Wie schon erwähnt werden alle Datentypen von der Klasse Object abgeleitet. Demnach ist es möglich die Methoden dieser Klasse auf einfache Weise aufzurufen. using System; class Test { static void Main() { Console.WriteLine(3.ToString()); } } Hier wird der Wert „3“ vom Typ int mit der Methode der Object Klasse einfach in ein String umgewandelt und ausgegeben. Es gibt darüber hinaus eine Möglichkeit der Umwandlung zwischen den Wertetypen und Referenztypen. Das Verfahren „boxing“ fällt eher in die Kategorie der Typumwandlung, zeigt aber wiederum die Vereinheitlichung der Datentypen: class Test { static void Main() { int i = 123; object o = i; // boxing int j = (int) o; // unboxing } } Beim „boxing“ Verfahren wird der Wert bildlich „in eine Box gesteckt“ (Objekt), braucht man dann wieder den Wert selbst, verwendet man „unboxing“, um ihn aus der Box herauszuholen. 5.3 Variablen und Parameter Die Variablen sind wie in allen prozedualen Sprachen Speicherplätze für die Werte. Alle Variablen sind in C# lokal, d.h. deklarierbar nur in Methoden, Eigenschaften und Indizierern. Beispiel: int i; class Test { static void Main() { int a; int b = 1; int c,d,e,f; } } //<- nicht erlaubt, da global Variablen werden als Felder (fields) bezeichnet, wenn sie mit einer Klasse oder Struktur assoziiert sind. Mit dem Modifizierer static versehenen Felder definieren statische Variablen (Zugreifbar über Klasse), Felder ohne diesen Modifizierer definieren Instanzvariablen (Zugreifbar über eine Instanz der Klasse). using Personnel.Data; class Employee { private static DataSet ds; public string Name; public decimal Salary; … } Formale Parameter der Methoden definieren ebenso Variablen, die von einem der vier Arten sein können: Wertparameter (value parameters), Referenzparameter (reference parameters), Ausgabeparameter (output parameters) oder Parameterarrays (parameter arrays). Bei dem Werteparameter wird ein Wert direkt an die Methode übergeben, um genauer zu sein eine Kopie – es findet keine Veränderung außerhalb der Methode statt. Ein Referenzparameter (ref) dagegen definiert keine Variable, er beinhaltet lediglich eine Referenz auf eine bereits deklarierte und initialisierte Variable. Die Veränderung der über den Referenzparameter übergebenen Variable ändert auch die referenzierte Variable außerhalb der Methode. using System; class Test { static void Swap(ref int a, ref int b) { int t = a; a = b; b = t; } static void Main() { int x = 1; int y = 2; Console.WriteLine("pre: x = {0}, y = {1}", x, y); Swap(ref x, ref y); //<-beachte - auch beim Aufruf - ref Console.WriteLine("post: x = {0}, y = {1}", x, y); } } Ausgabe: pre: x = 1, y = 2 post: x = 2, y = 1 Ausgabeparameter (out) sind Referenzparameter, nur müssen Sie vor der Übergabe an eine Methode nicht unbedingt initialisiert werden. using System; class Test { static void Divide(int a, int b, out int result, out int remainder) { result = a / b; remainder = a % b; } static void Main() { for (int i = 1; i < 10; i++) for (int j = 1; j < 10; j++) { int ans, r; Divide(i, j, out ans, out r); //<-beachte - auch beim Aufruf - out Console.WriteLine("{0} / {1} = {2}r{3}", i, j, ans, r); } } } Mit Parameterarrays (params) können mehrere Parameter vom gleichen Typ übergeben werden, dabei muss beachtet werden, dass ein Parameterarray immer als letzter in der Liste der Methodenparametern stehen muss, und dass es nur ein einziger eindimensionaler Array als Parameter agieren darf. using System; class Test { static void F(params int[] args) { Console.WriteLine("# of arguments: {0}", args.Length); for (int i = 0; i < args.Length; i++) Console.WriteLine("args[{0}] = {1}", i, args[i]); } static void Main() { F(); F(1, 2, 3); F(new int[] {1, 2, 3, 4}); } } Ausgabe: # of arguments: 0 # of arguments: 3 args[0] = 1 args[1] = 2 args[2] = 3 # of arguments: 4 args[0] = 1 args[1] = 2 args[2] = 3 args[3] = 4 5.4 Automatische Speicherverwaltung Wie in Java gibt, wird bei C# ein Garbage-Collector eingesetzt, der automatisch für die Speicherbereinigung sorgt und nicht mehr benötigte Objekte selbst löscht. Bei seiner Arbeit kann er auch andere Objekte im Speicher bewegen. Es ist aber trotzdem noch möglich, mit Zeigern wie in C++ zu arbeiten. Dazu müssen die entsprechenden Objekte im Speicher fixiert werden, um zu verhindern, dass sie vom Garbage-Collector von ihrem ursprünglichen Speicherort verschoben werden. Der Code mit Zeigern muss mit dem Schlüselwort unsafe markiert werden und wird in C# als „unsicher“ bezeichnet. using System; class Test { static void WriteLocations(byte[] arr) { unsafe { // unsicheren Code ankündigen fixed (byte* pArray = arr) { // Objekt fixieren byte* pElem = pArray; for (int i = 0; i < arr.Length; i++) { byte value = *pElem; Console.WriteLine("arr[{0}] at 0x{1:X} is {2}", i, (uint)pElem, value); pElem++; } } } } static void Main() { byte[] arr = new byte[] {1, 2, 3, 4, 5}; WriteLocations(arr); } } 5.5 Ausdrücke C# besitzt unäre, binäre und ternäre Operatoren, die Priorität der Auswerung ist absteigend in der unteren Tabelle angeordnet. Bei mehreren Operatoren mit gleicher Priorität in einem Ausdruck, gilt für den Zuweisungsoperator und dem Bedingungsoperator (?:) rechtsassoziative, bei allen andern linksassoziative Auswertung. Beispiel: x + y + z wird ausgewertet wie (x + y) + z. x = y = z wird ausgewertet wie x = (y = z). Prioritäten der Operatoren absteigend sortiert: Quelle: ECMA-334 5.6 Anweisungen (Statements) Die meisten Anweisungen wie for, while, if etc. entsprechen ihren Pedanten aus C/C++. Die in C# neu eingeführten sind hier aufgelistet. Labeled statements und goto statements foreach statements throw statements und try statements checked und unchecked statements lock statements using statements static void Main(string[] args) { if (args.Length == 0) goto done; System.Console.WriteLine(args.Length); done: System.Console.WriteLine("Done"); } static void Main(string[] args) { foreach (string s in args) System.Console.WriteLine(s); } static int F(int a, int b) { if (b == 0) throw new Exception("Divide by zero"); return a / b; } static void Main() { try { System.Console.WriteLine(F(5, 0)); } catch(Exception e) { Console.WriteLine("Error"); } } static void Main() { int x = Int32.MaxValue; System.Console.WriteLine(x + 1); // Overflow checked { System.Console.WriteLine(x + 1); // Exception } unchecked { System.Console.WriteLine(x + 1); // Overflow } } static void Main() { A a = …; lock(a) { a.P = a.P + 1; } } static void Main() { using (Resource r = new Resource()) { r.F(); } } Man beachte, dass die goto Anweisung aus Basic in C# übernommen wurde. 5.7 Klassen Klassendeklarationen definieren neue Referenztypen. Eine Klasse kann erben und kann Interfaces implementieren. In C# ist es wie in Java keine Mehrfachvererbung möglich, Implementierung mehrerer Interfaces dagegen schon. Klassenmember können Konstanten, Felder, Methoden, Eigenschaften, Ereignisse, Indizierer, Operatoren, Instanzkonstruktoren, Destruktoren, statische Konstruktoren und geschachtelte Typen (nested types) sein. Jedes der Klassenmember hat eigene Zugriffsmodifizierer. Es gibt fünf mögliche Formen der Zugriffsberechtigungen, die in der Tabelle zusammengefasst sind. public protected Uneingeschränkter Zugriff Zugriff ist auf die enthaltene Klasse oder auf Typen begrenzt, die von der enthaltenen Klasse abgeleitet sind internal Zugriff ist auf dieses Programm begrenzt protected internal Zugriff ist auf dieses Programm oder auf Typen begrenzt, die von der enthaltenden Klasse abgeleitet sind private Der Zugriff ist auf den enthaltenden Typ begrenzt 5.7.1 Konstanten (constants) Eine Konstante ist ein Klassenmember und repräsentieren einen konstanten Wert, d. h. einen Wert, der während der Kompilierung berechnet werden kann. Die Konstanten brauchen kein static Attribut, es wird auch nicht zugelassen. Der Zugriff auf Konstanten kann bei entsprechendem Modifizierer public auch von außerhalb der Klasse erfolgen. 5.7.2 Felder (fields) Ein Feld ist ein Klassenmember, der eine mit einem Objekt oder einer Klasse verknüpfte Variable repräsentiert. Einfacher gesagt sind es Variablen, die auf der Klassenebene deklariert sind. 5.7.3 Methoden (methods) Eine Methode ist ein Klassenmember, der die Berechnungen und Aktionen, die von der Klasse oder einem Objekt ausgeführt werden können, implementiert. Methoden haben eine Parameterliste (eventuell eine leere), einen Rückgabewert (void = kein Rückgabewert) und können entweder statisch oder nicht-statisch sein. Statische Methoden sind über die Klasse zugreifbar, nicht-statische nur über eine Instanz der Klasse – also über ein Objekt. using System; public class Stack { public static Stack Clone(Stack s) {…} public static Stack Flip(Stack s) {…} public object Pop() {…} public void Push(object o) {…} public override string ToString() {…} ... } Methoden können überladen werden – die Methoden dürfen den gleichen Namen haben, solange sie ihre eigene Signaturen haben. Die Signatur einer Methode besteht aus dem Namen der Methode sowie der Anzahl, den Modifizierern und den Typen der formalen Parameter. Beispiel zeigt eine Klasse mit mehreren Methoden namens F: using System; class Test { static void F() { Console.WriteLine("F()"); } static void F(object o) { Console.WriteLine("F(object)"); } static void F(int value) { Console.WriteLine("F(int)"); } static void F(ref int value) { Console.WriteLine("F(ref int)"); } static void F(int a, int b) { Console.WriteLine("F(int, int)"); } static void F(int[] values) { Console.WriteLine("F(int[])"); } static void Main() { F(); F(1); int i = 10; F(ref i); F((object)1); F(1, 2); F(new int[] {1, 2, 3}); } } Ausgabe: F() F(int) F(ref int) F(object) F(int, int) F(int[]) 5.7.4 Eigenschaften (properties) Eine Eigenschaft ist ein Klassenmember, welcher den Zugang zu Merkmalen (Eigenschaften) eines Objekts oder einer Klasse liefert. Eigenschaften sind die natürliche Erweiterung von Feldern. Beide sind benannte Member mit verknüpften Typen und die Syntax für den Zugriff auf Felder und Eigenschaften ist identisch. Im Unterschied zu Feldern kennzeichnen Eigenschaften jedoch keine Speicherorte. Stattdessen besitzen Eigenschaften Zugriffsmechanismen (accessors), die die Anweisungen angeben, die beim Lesen bzw. Schreiben ihrer Werte auszuführen sind. Beispiel: public class Button { private string caption; public string Caption { get { return caption; } set { caption = value; Repaint(); } // Feld // Eigenschaft } ... } Eigenschaften, die sowohl geschrieben als auch gelesen werden können, haben get und set Accessoren. Der Accessor get wird aufgerufen sobald ein Lesezugriff auf die Eigenschaft erfolgt, set – wenn es sich um einen Schreibzugriff handelt. Das Lesen und Schreiben erfolgt wie bei normalen Feldern. Button b = new Button(); b.Caption = "ABC"; // set; Repaint(); string s = b.Caption; // get; b.Caption += "DEF"; // get & set; Repaint(); 5.7.5 Ereignisse (events) Ein Ereignis (event) ist ein Klassenmember, der einem Objekt oder einer Klasse das Bereitstellen von Benachrichtigungen ermöglicht. Die Deklaration einer Eigenschaft sieht wie die Deklaration eines Feldes mit dem vorangestellten Schlüsselwort event aus. Der Typ einer Ereignis - Deklaration muss ein Delegat sein. In dem Beispiel public delegate void EventHandler(object sender, System.EventArgs e); public class Button { public event EventHandler Click; public void Reset() { Click = null; } } definiert die Klasse Button ein Click-Ereignis vom Typ EventHandler. Die Verwendung der Ereignisse kann so aussehen: using System; public class Form1 { public Form1() { Button1.Click += new EventHandler(Button1_Click); } Button Button1 = new Button(); void Button1_Click(object sender, EventArgs e) { Console.WriteLine("Button1 was clicked!"); } public void Disconnect() { Button1.Click -= new EventHandler(Button1_Click); } } Näheres wird im ECMA – Standard beschrieben. 5.7.6 Operatoren (operators) Ein Operator ist ein Member, der die Bedeutung eines Ausdrucksoperators definiert, der für die Instanzen der Klasse angewendet werden kann. Es gibt drei Kategorien von Operatoren: unäre Operatoren, binäre Operatoren und Konvertierungsoperatoren. Der folgende Beispiel definiert einen Digit Typ, der Dezimalwerte zwischen 0 und 9 repräsentiert. using System; public struct Digit { byte value; public Digit(byte value) { if (value < 0 || value > 9) throw new ArgumentException(); this.value = value; } public Digit(int value): this((byte) value) {} public static implicit operator byte(Digit d) { return d.value; } public static explicit operator Digit(byte b) { return new Digit(b); } public static Digit operator+(Digit a, Digit b) { return new Digit(a.value + b.value); } public static Digit operator-(Digit a, Digit b) { return new Digit(a.value - b.value); } public static bool operator==(Digit a, Digit b) { return a.value == b.value; } public static bool operator!=(Digit a, Digit b) { return a.value != b.value; } public override bool Equals(object value) { if (value == null) return false; if (GetType() == value.GetType()) return this == (Digit)value; return false;} public override int GetHashCode() { return value.GetHashCode(); } public override string ToString() { return value.ToString(); } } class Test { static void Main() { Digit a = (Digit) 5; Digit b = (Digit) 3; Digit plus = a + b; Digit minus = a - b; bool equals = (a == b); Console.WriteLine("{0} + {1} = {2}", a, b, plus); Console.WriteLine("{0} - {1} = {2}", a, b, minus); Console.WriteLine("{0} == {1} = {2}", a, b, equals); } } Der Digit Typ definiert folgende Operatoren: • Einen Operator für die implizite Umwandlung von Digit in byte. • Einen Operator für die explizite Umwandlung von byte in Digit. • Einen Plus-Operator zum Addieren von zwei Digit Werten, gibt einen Digit Wert zurück. • Einen Minus-Operator zum Subtrahieren eines Digit Wertes vom andern, gibt einen Digit Wert zurück. • Einen Gleichheits- (==) und Ungleichheits- (!=) Operator, der zwei Digit Werte vergleicht. 5.7.7 Indizierer (indexers) Ein Indizierer ist ein Member, der es einem Objekt ermöglicht, auf die gleiche Art wie ein Array indiziert zu werden. Während Eigenschaften einen Feld-ähnlichen Zugriff ermöglichen, bieten Indizierer einen Array-ähnlichen Zugriff. Ein entsprechendes Beispiel zeigt, wie bei den Indizierern die von den Eigenschaften bekannten Mechanismen get und set verwendet werden. Der indizierender Parameter befindet sich in den eckigen Klammern. using System; public class Stack { private Node GetNode(int index) { Node temp = first; while (index > 0) { temp = temp.Next; index--; } return temp; } public object this[int index] { get { if (!ValidIndex(index)) throw new Exception("Index out of range."); else return GetNode(index).Value; } set { if (!ValidIndex(index)) throw new Exception("Index out of range."); else GetNode(index).Value = value; } } ... } class Test { static void Main() { Stack s = new Stack(); s.Push(1); s.Push(2); s.Push(3); s[0] = 33; // Oberes Element: 3 -> 33 s[1] = 22; // Mittleres Element: 2 -> 22 s[2] = 11; // Unteres Element: 1 -> 11 } } 5.7.8 Instanzkonstruktoren (instance constructors) Ein Instanzkonstruktor ist ein Member, der die zur Initialisierung einer Klasseninstanz erforderlichen Aktionen implementiert. using System; class Point { public double x, y; public Point() { this.x = 0; this.y = 0; } public Point(double x, double y) { this.x = x; this.y = y; } ... } Die Klasse Point hat zwei Instanzkonstruktoren – einen leeren und einen, der zwei Argumente erwartet. Die eigentliche Verwendung wird im folgenden Code deutlich: class Test { static void Main() { Point a = new Point(); Point b = new Point(3, 4); … } } je nach Aufruf wird der entsprechende Instanzkonstruktor aufgerufen. Falls eine Klasse keinen Instanzkonstruktor definiert, wird automatisch ein Instanzkonstruktor ohne Argumente zur Verfügung gestellt. 5.7.9 Destruktoren (destructors) Ein Destruktor ist ein Member, der die Aktionen implementiert, die zum Zerstören einer Klasseninstanz erforderlich sind. Destruktoren erlauben keine Argumente, sie besitzen keine Zugriffsmodifizierer und können nicht explizit aufgerufen werden. Der Destruktor einer Klasseninstanz wird automatisch aufgerufen, wenn der Garbage-Collector tätig wird. Die Klasse Point wird um den Destruktor erweitert: using System; class Point { public double x, y; public Point() { this.x = 0; this.y = 0; } public Point(double x, double y) { this.x = x; this.y = y; } ~Point() { Console.WriteLine("Destructed {0}", this); } ... } 5.7.10 Statische Konstruktoren (static constructors) Ein statischer Konstruktor ist ein Member, der die Aktionen implementiert, die zum Initialisieren einer Klasse erforderlich sind. Statische Konstruktoren erlauben keine Argumente, sie besitzen keine Zugriffsmodifizierer und können nicht explizit aufgerufen werden. Sie werden automatisch aufgerufen, sobald eine Instanz der Klasse erstellt wird. Beispiel: using Personnel.Data; class Employee { private static DataSet ds; static Employee() { ds = new DataSet(…); } public string Name; public decimal Salary; … } Sobald eine Instanz der Klasse Employee erstellt wird, wird der statische Konstruktor aufgerufen und legt einen neuen Datensatz vom Typ DataSet an. 5.7.11 Vererbung (inheritance) Alle Klassen unterstützen Einfachvererbung und der Typ object ist die ultimative Basisklasse für alle Klassen. Alle Klasse, die in frühen Beispielen vorgestellt wurden, sind von object abgleitet. using System; class A { public void } class B: A { public void } class Test { static void B b = new b.F(); b.G(); } } F() { Console.WriteLine("A.F"); } G() { Console.WriteLine("B.G"); } Main() { B(); // geerbt von A // enthalten in B Klasse A ist von object abgeleitet , Klasse B dagegen von A. 5.8 Strukturen (structs) Strukturen sind den Klassen sehr ähnlich, sie können auch Interfaces implementieren und ebenso die gleichen Memeber wie die Klassen besitzen. Strukturen unterschieden sich jedoch in einigen wichtigen Punkten von den Klassen: • Strukturen sind Wertetypen, Klassen sind dagegen Referenztypen • Vererbung ist bei Strukturen nicht möglich Einen „Punkt“ kann man als eine Klasse oder eine Struktur realisieren. Als Klasse: class Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } class Test { static void Main() { Point[] points = new Point[100]; for (int i = 0; i < 100; i++) points[i] = new Point(i, i*i); } } Als Struktur: struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } Man verwendet in der Regel Strukturen immer dann, wenn ein Objekt gewünscht wird, das sich wie ein einfacher Datentyp verhält, d.h. ohne viele Ressourcen auskommt und schnell zugeordnet werden kann. 5.9 Schnittstellen (interfaces) Eine Schnittstelle definiert einen Vertrag. Eine Klasse oder Struktur, die eine Schnittstelle implementiert, muss ihren Vertrag einhalten. Schnittstellen können Methoden, Eigenschaften, Ereignisse und Indizierer als Member enthalten. Beispiel für ein Interface mit einem Indizierer, einem Ereignis E, einer Methode F und einer Eigenschaft P: interface Iexample { string this[int index] { get; set; } event EventHandler E; void F(int value); string P { get; set; } } public delegate void EventHandler(object sender, EventArgs e); Bei den Schnittstellen ist eine Mehrfachvererbung möglich: interface Icontrol { void Paint(); } interface ITextBox: Icontrol { void SetText(string text); } interface IListBox: Icontrol { void SetItems(string[] items); } interface IComboBox: ITextBox, IListBox {} Eine Klasse kann auch mehrere Interfaces implementieren: interface IDataBound{ void Bind(Binder b); } public class EditBox: Control, IControl, IdataBound { public void Paint() {…} public void Bind(Binder b) {…} } 5.10 Delegaten (delegate) Delegaten ermöglichen Szenarien, für die in anderen Sprachen Funktionszeiger verwendet wurden. Delegaten sind im Gegensatz zu Funktionszeigern in C++ vollständig objektorientiert und sind typensicher. Die Verwendung von Delegaten erfolgt in drei Schritten: Deklaration, Instanziierung und Aufruf. Dies wird am folgenden Beispiel verdeutlicht: delegate void SimpleDelegate(); class Test { static void F() { System.Console.WriteLine("Test.F"); } static void Main() { SimpleDelegate d = new SimpleDelegate(F); d(); } } // Deklaration // Instanziierung // Aufruf Eine Delegatdeklaration definiert eine Klasse, die von der System.Delegate-Klasse abgeleitet ist. Eine Delegatinstanz enthält eine Aufrufliste mit einer oder mehreren Methoden, auf die verwiesen wird. Beim Aufruf einer Delegatinstanz mit einem entsprechenden Satz von Argumenten werden alle aufrufbaren Methoden des Delegaten mit dem vorgegebenen Satz von Argumenten aufgerufen. Eine interessante und nützliche Eigenschaft einer Delegatinstanz ist die Tatsache, dass sie die Klassen der Methoden, die sie einschließt, nicht kennt oder sich nicht dafür interessiert. Die einzige Voraussetzung besteht darin, dass die Methoden mit dem Typ des Delegaten kompatibel sind. Das macht Delegaten perfekt für "anonyme" Aufrufe geeignet. 5.11 Enumerationen (enums) Ein Enumerationstyp ist ein bestimmter Werttyp, der eine Reihe von benannten Konstanten deklariert. Die Enumerationen werden meist zur besseren Lesbarkeit des Quelltexts verwendet. Außerdem können „intelligente“ Editoren anhand von enums die möglichen Werte bei ihrer Verwendung vorschlagen. Ein Beispiel zur Deklaration und Verwendung von Enumerationen: enum Color { Red, Blue, Green } class Shape { public void Fill(Color color) { switch(color) { case Color.Red: … break; case Color.Blue: … break; case Color.Green: … break; default: break; } } } 5.12 Namespaces und Assemblies C# - Programme werden mit Hilfe von Namespaces organisiert. Sie werden sowohl als "internes" Organisationssystem für ein Programm als auch als "externes" Organisationssystem verwendet. Das externe System bietet eine Möglichkeit, Programmelemente darzustellen, die für andere Programme offen gelegt werden. Assemblies werden zur physikalischen Trennung der Programmkomponenten in Baugruppen verwendet. Ein Assembly kann Typen, ausführbaren Code für die Implementierung dieser Typen und Referenzen zu anderen Assemblies enthalten. Um das Prinzip der Trennung zu demonstrieren, wird das schon bekannte Programm „Hallo Welt“ in zwei Teile gesplittert: eine Klassenbibliothek, die die Nachricht bereitstellt und eine Konsolenanwendung, die diese Nachricht anzeigt. Die Klassenbibliothek wird eine Klasse HelloMessage enthalten. // HelloLibrary.cs namespace CSharp.Introduction { public class HelloMessage { public string Message { get { return "Hallo Welt!"; } } } } //neuer Namespace wird definiert //Eigenschaft der Klasse (read-only) Als Nächstes wird die Anwendung erstellt, die diese Klasse verwendet. Der Zugriff auf die Klasse erfolgt normalerweise über CSharp.Introduction.HelloMessage, doch man kann die using namespace Direktive verwenden um das Ganze zu verkürzen, wie es bei den Beispielen mit der Ausgabe auf der Konsole schon gezeigt wurde. // HelloApp.cs using CSharp.Introduction; //Verwendung vom Namespace CSharp.Introduction class HelloApp { static void Main() { HelloMessage m = new HelloMessage(); System.Console.WriteLine(m.Message); } } Jetzt kann der Quellcode kompiliert werden. Und zwar zum einen in eine Klassenbibliothek und zum andern in die Konsolenanwendung mit der Klasse HelloApp. Beim .NET-Framework kann das durch folgende Anweisungen geschehen: csc /target:library HelloLibrary.cs csc /reference:HelloLibrary.dll HelloApp.cs Als Ergebnis wird eine Klassenbibliothek namens HelloLibrary.dll und eine Anwendung namens HelloApp.exe erstellt. 5.13 Versioning Beim Versioning handelt es sich um den Prozess der Verfolgung einer Komponente, um die Kompatibilität der Komponente sicherzustellen. Eine neue Version einer Komponente ist quellenkompatibel mit einer vorherigen Version, wenn der von der alten Version abhängige Code nach der erneuten Kompilierung auch in der neuen Version funktioniert. Im Gegensatz dazu ist die neue Version einer Komponente binärkompatibel, wenn die von der alten Version anhängige Anwendung ohne erneute Kompilierung mit der neuen Version funktioniert. Ein ausführliches Beispiel ist in dem ECMA-334 Dokument enthalten. 5.14 Attribute (attributes) Ein großer Teil der Sprache C# ermöglicht dem Programmierer das Festlegen von Deklarationsinformationen zu den im Programm definierten Elementen. So wird beispielsweise die Zugriffsmöglichkeit einer Methode in einer Klasse durch die Ergänzung von Methodenmodifizierern wie public, protected, public, protected oder internal festgelegt. C# ermöglicht Programmierern auch die Einführung neuer Arten von Deklarationsinformationen, die als Attribute bezeichnet werden. Programmierer können dann diese Attribute mit verschiedenen Programmelementen verbinden und in einer Laufzeitumgebung Attributinformationen abrufen. Eins wohl der wichtigsten Attribute ist sysimport. Damit ist es möglich, die in systemeigenem Code geschriebene Funktionen aufzurufen. Der Typ und der Name der Datei werden als Parameter des sysimport-Attributs angegeben. Das Beispiel zeigt einen Aufruf der „windowseigenen“ Messagebox: class Test { [sysimport(dll="user32.dll")] public static extern int MessageBoxA(int h, string m, string c, int type); public static void Main() { int ret = MessageBoxA(0, "Hallo Welt!", "Caption", 0); } } 6. Quellen • ECMA-334 Standard „C# Language Specification“ http://www.ecma-international.org/publications/standards/ecma-334.htm • C# - Programmiersprachespezifikation http://msdn.microsoft.com/library/deu/default.asp?url=/library/DEU/csspec/html/CSha rpSpecStart.asp • Eric Gunnerson, C# - Tutorial und Referenz http://www.galileocomputing.de/openbook/csharp/ • Guide to C# http://www.golohaas.de/csharp • News Archiv von heise.de http://www.heise.de/newsticker/archiv