Aus Linux-Magazin 12/2008

C#-Entwicklung unter Linux - Teil 2

© microimages, Fotolia.com

Mit C# hat Microsoft eine C++-ähnliche Programmiersprache geschaffen, die die Erfahrungen aus über zwölf Jahren Java beherzigt. So garantieren zum Beispiel Interfaces, dass sich Objekte eines unterschiedlichen Typs vergleichen lassen. Manches C++-Feature, das Java fehlt, gibt es in C# obendrauf .

C# legt mehr Gewicht auf die Objektorientierung als C++, das die Kompatibilität zur prozeduralen Sprache C wahrt. Alle Klassen beerben daher in C# die Systemklasse »Object«. So lassen sich Objekte jedes Datentyps ebenso wie Referenzen darauf einfach in einem Array speichern. Außerdem stehen in jedem Objekt aus der Systemklasse geerbte Standardmethoden zur Verfügung.

Vorarbeiten

Teil 1 dieses Tutorials im vorigen Heft demonstrierte dies an einem Beispiel. Dort speichert ein Array »obstSammlung[]« Objekte der beiden Typen »Apfel« und »Birne« [1]. Für jedes Element steht die aus der Systemklasse geerbte Methode »ToString()« zur Verfügung.

Allerdings zeigt sich, dass die Standardmethode sehr generisch arbeitet. Der Cast eines Objekts in einen String gibt lediglich den qualifizierten Namen inklusive Namensraum zurück. Die Methode aus der Standardklasse »Object« kennt keine Details der vom Benutzer implementieren Klasse, sodass der zurückgegebene Wert wenig informativ ausfällt.

Da die aus »Object« geerbte Methode »ToString()« jedoch virtuell ist, lässt sich ihre Funktion mit den Mitteln der Objektorientierung verbessern: Der Programmier kann sie in von ihm definierten Klassen neu implementieren.Das Runtime Environment sucht zur Laufzeit die passende Variante der Methode heraus. Trifft sie auf eine Referenz auf ein Basisobjekt (hier vom Typ »Object«), die auf ein Objekt eines abgeleiteten Typs verweist, so prüft sie beim Aufruf einer virtuellen Methode, ob diese nicht in der abgeleiteten Klasse neu definiert ist.

Der Wurm drin

Listing 1 demonstriert das Überschreiben von vererbten virtuellen Methoden. Das Schlüsselwort »override« in der Deklaration von »Birne.ToString()« signalisiert dem Compiler, dass die neue Methode »ToString()« an die Stelle von »Object.ToString()« treten soll.

Listing 1: Methoden
überschreiben

01   // [...]
02   class Birne {
03     public Birne()
04     { Console.WriteLine("Im Birnen-Konstruktur: {0}", secret_); }
05 
06     public override string ToString() {
07       return secret_;
08     }
09     private string secret_ = "Ich bin eine Birne";
10   }
11   // [...]

Die Methode, die Obst-Objekte in einem Array vom Basistyp »Object« zu speichern, hat einen grundlegenden Nachteil: Die Ableitung des Array »Obstsammlung« vom Basistyp »Object« erlaubt es, beliebige Objekttypen darin zu speichern, statt, wie es sich für eine Obstsammlung gehört, nur Objekte vom Typ »Obst«. Damit geht ein wesentlicher Vorteil der Sprachen aus der C-Familie, die Typsicherheit, verloren.

Außerdem weist bisher allenfalls der Name von »Apfel« und »Birne« auf einen gemeinsamen Grundtyp hin. Dem objektorientierten Paradigma zufolge sollten sie jedoch von einer gemeinsamen Basisklasse »Obst« abstammen, um gemeinsame Eigenschaften nur einmal implementieren zu müssen.

Natürlich wäre es möglich, ein Array des Basistyps »Obst« zu erstellen, das nur von »Obst« abstammende Objekte akzeptieren würde. Die Einschränkung, dass die Deklaration die Länge des Array unveränderlich festlegt, lässt sich jedoch nur mit den in C# enthaltenen Containerklassen überwinden.

Diese verhalten sich grundsätzlich wie Arrays, bieten aber viele weitere Funktionen. Sie lassen sich beispielsweise durchsuchen und, wenn ein geeignetes Sortierkriterium existiert, auch sortieren. Last but not least kann sich ihre Größe zur Laufzeit ändern.

Sammelwut

Mit so genannten Generics, die C++-Programmierer unter dem Namen Templates kennen, ist es möglich, Containerklassen auf einen gewünschten Typ festzulegen. Der Entwickler braucht diesen bei der Deklaration bloß anzugeben. Der Compiler sorgt dann dafür, dass nur passende Objekte im Container landen. Ist dies nicht der Fall, bricht er die Übersetzung mit einem Fehler ab.

Der erste Schritt für eine erweiterbare Obstsammlung ist eine Basisklasse »Obst«. Listing 2 legt gemeinsame Eigenschaften fest, die Äpfel, Birnen und alle weiteren Obstsorten teilen. Objekte vom Typ »Obst« haben eine bestimmte Größe. Das Beispiel geht davon aus, dass das Obst noch am Baum hängt und wächst. Seine Größe ist also abhängig vom Alter und einer Anfangsgröße.

Privatheit stets gewährleistet

In C++ ließe sich dies über die privaten Variablen »double anfangsgroesse« und »double alter« modellieren. Aus Alter und Anfangsgröße berechnet sich die Größe zu jedem gewünschten Zeitpunkt. Für den Zugriff auf die privaten Elemente wäre eine Reihe von Methoden wie »setAlter()« oder »getAlter()« nötig. Dass es für derartige Methoden keine verbindlichen Namenskonventionen gibt, erschwert jedoch die Übersicht und die Wartbarkeit des Code.

Abhilfe schaffen in C# die so genannten Properties. Sie ermöglichen es, Eigenschaften wie das Alter des Obst-Objekts nach außen hin wie herkömmliche Variablen zu behandeln: Hinter den Kulissen läuft beim Zugriff dennoch wie bei den Zugriffsmethoden in C++ ein Prüfcode ab, nämlich die auf die zwischen geschweiften Klammern auf die Property-Deklarationen folgenden »get«- und »set«-Methoden (Listing 2, Zeilen 27 bis 34). Streng genommen handelt es sich bei »get« und »set« nicht um Funktionen, da sie keine Parameter entgegennehmen.

Nur wenn das zugewiesene Alter größer null ist, setzt Zeile 33 die Variable »alter_«. Ansonsten passiert gar nichts. In der Praxis müsste ein Programm in diesem Fall mit einer Exception reagieren. Die Property »Anfangsgroese« bildet Listing 2 intern auf die Variable »anfangsgroesse_« ab. Die Deklaration der »set«-Methode als »protected« bewirkt übrigens, dass nur abgeleitete Klassen und natürlich die Klasse selbst auf sie zugreifen dürfen. Eine Deklaration als »public« erlaubt dagegen den Zugriff auf die Properties an jeder Stelle des Code. »private« verbietet jeden Zugriff von außerhalb der Klasse.

Listing 2: Basisklasse
»Obst«

01   // [...]
02   public abstract class Obst : IComparable<Obst>
03   {
04     public Obst(double anfangsgroesse) {
05       this.Anfangsgroesse = anfangsgroesse;
06     }
07 
08     public double Anfangsgroesse {
09       get {
10         return anfangsgroesse_;
11       }
12 
13       protected set {
14         if(value > 0.0) {
15           anfangsgroesse_ = value;
16         }
17       }
18     }
19 
20     public double Groesse {
21       get {
22         return Math.Round(anfangsgroesse_ + 0.1 * alter_, 2);
23       }
24     }
25 
26     public double Alter {
27       get {
28         return alter_;
29       }
30 
31       set {
32         if(value >= 0.0) {
33           alter_ = value;
34         }
35       }
36     }
37 
38     public int CompareTo(Obst o){
39       return o.Groesse.CompareTo(this.Groesse);
40     }
41 
42     public static bool operator > (Obst o1, Obst o2) {
43       return o1.Groesse > o2.Groesse;
44     }
45 
46     public static bool operator < (Obst o1, Obst o2) {
47       return o1.Groesse < o2.Groesse;
48     }
49 
50     public override string ToString(){
51       Type t = this.GetType();
52       string result = "Ich bin Obst vom Typ " + t.Name + " und der Groesse " + this.Groesse;
53       return result;
54     }
55 
56     private double anfangsgroesse_ = 1.0;
57     private double alter_ = 0.0;
58   }
59   // [...]

Die Eigenschaft »Groesse« berechnet sich aus der Anfangsgröße und dem Alter des Obsts und braucht daher keine zusätzliche interne Variable und keine »set«-Funktion. Die der Klasse »Math« entstammende Methode »Round()« beschränkt den Wert auf zwei Nachkommastellen.

Äpfel mit Birnen vergleichen

Mit der Eigenschaft »Groesse« sorgt die Basisklasse »Obst« für eine gemeinsame Eigenschaft, über die sich Äpfel und Birnen vergleichen lassen. Dies ist unter anderem auch Voraussetzung für ein Sortieren mehrerer Obstsorten in einem Container.

Am intuitivsten wäre es, wenn die Vergleichsoperationen »größer als« und »kleiner als« für die Klasse »Obst« selbst definiert wäre, nicht bloß für deren Eigenschaft »Groesse«. Da C# wie C++, aber anders als Java das Überladen von Operatoren erlaubt, funktioniert nach der klassenspezifischen Definition (Listing 2, Zeilen 42 bis 48) des »>«- und »<«-Operators der Code »if(apfel < birne) […]« und »if(apfel > birne) […]«

Die Klasse »System.Collections.Generic.List«, die später als Container für die Obst-Objekte zum Einsatz kommt, setzt außerdem zwingend voraus, dass in ihr gespeicherte Objekte das Interface »IComparable<T>« umsetzen. Eine »using System.Collections.Generic;«-Anweisung zu Beginn des Programmcode verkürzt den Klassennamen auf »List«.

Ein Interface (Abbildung 1) stellt innerhalb von Klassenhierarchien sicher, dass alle beteiligten Klassen bestimmte, dort festgelegte Methoden zur Verfügung stellen. Ein Container-Objekt, das eine Methode zum Sortieren bereitstellt, gerät schließlich in Schwierigkeiten, wenn sich nicht alle in ihr enthaltenen Elemente über einen definierten Wert vergleichen lassen. Das Interface »IComparable<T>« fordert daher das Vorhandensein der Methode »int CompareTo(T)«. Diese liefert numerische Vergleichswerte. Der Zusatz »<T>« legt den Typ der zu vergleichenden Objekte fest.

Abbildung 1: Objekte erben oft einen Grundbestand an Eigenschaften und Methoden von Basisklassen. Interfaces garantieren zusätzlich das Vorhandensein bestimmter Methoden. So verbindet Äpfel und Birnen oder andere sonst disparate Objekte eine garantierte Vergleichsmethode.

Abbildung 1: Objekte erben oft einen Grundbestand an Eigenschaften und Methoden von Basisklassen. Interfaces garantieren zusätzlich das Vorhandensein bestimmter Methoden. So verbindet Äpfel und Birnen oder andere sonst disparate Objekte eine garantierte Vergleichsmethode.

Das Interface selbst spezifiziert nur Namen und Signatur der von ihm geforderten Funktionen. Die Implementierung steht erst in den Klassen, die das Interface umsetzen. Leitet sich eine Klasse von einem Interface ab (Zeile 2 in Listing 2), so ist dadurch jedoch bereits beim Übersetzen des Code garantiert, dass sie die nötige Funktionalität zur Laufzeit zur Verfügung stellt. Interfaces sind also mit C++-Klassen vergleichbar, in denen Funktionen lediglich als Platzhalter wie »virtual void myFunction(void) = 0;« implementiert sind.

Abstrakt gedacht

Außer dem angegebenen Interface fällt in der Klassendeklaration in Zeile 2 das Schlüsselwort »abstract« auf. Es verhindert, dass sich Objekte vom Typ »Obst« direkt mit »new« instanziieren lassen. Erst von »Obst« abgeleitete Klassen sind instanziierbar, sofern sie nicht selbst wieder abstrakt sind. Dies entspricht der Realität, wo es kein Obst an sich gibt, sondern nur Äpfel, Birnen oder Trauben. Auch hier zeigt sich C# im Vergleich zu C++ überlegen. Dort lassen sich abstrakte Klassen nur über den gezeigten Umweg mit minimalen virtuellen Platzhalterfunktion wie der gerade genannten »myFunction()« umsetzen.

Eine weiteres Feature, das C# aus der Java-Welt übernimmt, ist Introspektion, also die Fähigkeit eines Programms, seine eigene Struktur zu reflektieren oder zu verändern. Listing 2 nutzt dies, um der rudimentären »ToString()«-Funktion in Listing 1 endlich eine sinnvolle Ausgabe zu verpassen. Eine »using System.Reflection;«-Direktive muss dazu am Anfang des Programmcode das Reflection Framework zugänglich machen. Zeile 52 fragt nach dem Typ des der Funktion übergebenen Objekts und bindet ihn in die Ausgabe ein.

Vom Obst zu Äpfeln und Birnen

Listing 3 leitet aus der abstrakten Obst-Klasse die instanziierbaren Klassen »Apfel« und »Birne« ab und fügt dem Umfang der Basisklasse noch einige sortenspezifische Eigenschaften hinzu. Die Ableitung von der Obst-Klasse durch »Apfel« und »Birne« ist weitgehend selbsterklärend. Der Konstruktor von »Apfel« und »Birne« initialisiert lediglich die »Anfangsgroesse« der Obst-Klasse. Auf den Konstruktor der Basisklasse greift er dabei über das Schlüsselwort »base« zu (Zeilen 4 und 14). Bemerkenswert ist noch die Property »ZahlDerKerne« (Zeile 7). Sie zeigt, dass der Programmierer das Erzeugen der Get- und Set-Methoden auch dem Compiler überlassen kann. Auch eine interne Variable zum Speichern des Werts ist nicht erforderlich.

Listing 3: Abgeleitete
Klassen

01   public class Apfel: Obst
02   {
03     public Apfel()
04       :base(2.0)
05       { /* nothing */ }
06 
07     public uint ZahlDerKerne
08     { get; set; }
09   }
10 
11   public class Birne: Obst
12   {
13     public Birne()
14       :base(2.2)
15       { /* nothing */ }
16 
17     public double StielLaenge {
18       get {
19         return stielLaenge_;
20       }
21       set {
22         if(value >= 0.0) stielLaenge_ = value;
23       }
24     }
25 
26     private double stielLaenge_ = 1.0;
27   }

Ein einfacher Wertecheck ergibt sich dennoch durch die Deklaration der Eigenschaft als »uint«, also als unsigned Integer (Zeile 7): Negative Werte für die Zahl der Kerne sind nicht sinnvoll. Eine Zuweisung von »-1« führt daher (zumindest unter Mono) zu einer Fehlermeldung, die explizite Wertechecks überflüssig macht.

Wohlgeordnet

Listing 4 erzeugt zunächst mit Hilfe einer Schleife und des Zufallsgenerators eine Reihe von Apfel- und Birne-Objekten.Die Klasse »ObstDrucker« enthält zum Speichern der im Konstruktor erzeugten Apfel- und Birne-Objekte ein privates Container-Objekt des Typs »List<Obst>« (Zeile 31). »<Obst>« gibt dabei den Typ der Objekte an, die der Container aufnimmt. Da Äpfel und Birnen von der Obst-Klasse abgeleitet sind, akzeptiert sie das Container-Objekt.

Listing 4:
Obstkollektion

01   class ObstDrucker {
02     public ObstDrucker(){
03       Random rg = new Random();
04 
05       for(uint i=0; i<4; i++){
06         if(i%2 == 0){
07           Apfel apfel = new Apfel();
08           apfel.Alter = 50.0 * rg.NextDouble();
09           apfel.ZahlDerKerne = (uint)rg.Next(10); // Maximal 9 Kerne
10           obstSammlung.Add(apfel);
11         }
12         else {
13           Birne birne = new Birne();
14           birne.Alter = 50.0 * rg.NextDouble();
15           birne.StielLaenge = rg.NextDouble(); // Maximal 1 cm
16           obstSammlung.Add(birne);
17         }
18       }
19     }
20 
21     public void sortiereObst(){
22       obstSammlung.Sort();
23     }
24 
25     public void print(){
26       foreach(Obst o in obstSammlung) {
27         Console.WriteLine(o.ToString());
28       }
29     }
30 
31     private List<Obst> obstSammlung = new List<Obst>();
32   }

Der Konstruktor der »ObstDrucker«-Klasse (Zeilen 2 bis 19) erzeugt mit der Random-Klasse einen Zufallszahlengenerator. Dann generiert er durch die Modulo-2-Prüfung abwechselnd Äpfel und Birnen. Jedem Obststück weist der Zufallszahlengenerator ein Alter zu. Die »Add()«-Methode verleibt die Früchte dem Container »List<Obst>« ein.

Dank der »CompareTo()«-Methode, die das Interface »IComparable« fordert, lassen sich die unterschiedlichen Obstsorten miteinander vergleichen: Äpfel, Birnen, und, wären sie implementiert, auch Trauben. »sortiereObst()« (Zeile 21) ordnet die Obstsammlung daher entsprechend der Umsetzung der »CompareTo()«-Methode nach der Größe. Dazu muss sie nichts anderes tun, als die »Sort()«-Methode des Containers aufrufen (Zeile 22).

Die Methode »print()« ruft schließlich über die »foreach«-Schleife die Methode »ToString()« für jedes Element des Obst-Containers auf und gibt so den Objekttyp und die Eigenschaft »Groesse« auf der Konsole aus. Nun fehlt nur noch eine einfache »Main()«-Funktion, die ein »ObstDrucker«-Objekt erzeugt und dessen »print()«-Funktion aufruft (Listing 5). Zeile 9 sortiert die Liste, ein erneuter Aufruf von »print()« zeigt das Ergebnis (Abbildung 2).

Das beiden Dateien »main.cs« und »obstSammlung.cs« unter [2] enthalten den Code aller Listings dieses Workshops. »gmcs main.cs obstSammlung.cd« kompiliert ihn, »mono main.exe« startet das übersetzte Programm.

Abbildung 2: Lohn der Mühe: Das in diesem Artikel entstandene Programm demonstriert Containerklassen mit eingebauter Sortierung sowie einfache Introspektion.

Abbildung 2: Lohn der Mühe: Das in diesem Artikel entstandene Programm demonstriert Containerklassen mit eingebauter Sortierung sowie einfache Introspektion.

Ausblick

Dieser Teil der vierteiligen Mono-Reihe hat viele wichtige Merkmale der von ihrem Design her zwischen C++ und Java angesiedelten Sprache C# vorgestellt. Mit etwas anderweitiger Programmiererfahrung sollte das dabei erworbene Wissen für die Lösung anspruchsvoller Probleme reichen.

Der folgende dritte Teil des Tutorials im nächsten Linux-Magazin wird sich mit dem Problem der Persistenz beschäftigen, also der Frage, wie sich der Zustand von Objekten zwischenspeichern lässt. Dabei wird es um C#’s eigene Mechanismen zur Serialisierung gehen, also der Umwandlung des Zustands von Objekten in einen abspeicherbaren Datenstring. Außerdem wird der Artikel den Zustand von Objekten als XML-Code speichern und einen Parser vorstellen, der daraus den ursprünglichen Programmzustand wiederherstellt. (pkr)

Infos

[1] Code des vorigen Tutorials: [ftp://linux-magazin.de/pub/magazin/2008/11/Mono-Workshop/]

[2] Quellcode: [ftp://linux-magazin.de/pub/magazin/2008/12/Mono-Workshop2/]

Der Autor:

Dr. Rüdiger Berlich arbeitet seit 1992 mit Linux und Open Source. Er bereitet am Karlsruhe Institute of Technology einen Open-Source-Spinoff aus dem Bereich der verteilten Optimierung vor.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben