4 Weitere .NET-Datentypen
4.1 Interfaces (Schnittstellen) 

Das Konzept der Schnittstellen ist am einfachsten zu verstehen, wenn man sich deutlich macht, worin genau der Unterschied zwischen einer Klasse und einem Objekt besteht. Klassen sind Schablonen, in denen Methoden und Eigenschaften definiert sind. Die Methoden manipulieren die Eigenschaften und stellen damit das Verhalten eines Objekts sicher. Ein Objekt wird jedoch nicht durch sein Verhalten, sondern durch seine Daten beschrieben, die über Eigenschaften manipuliert werden.
Treiben wir die Abstraktion noch weiter. Wenn sich ein Objekt durch Daten beschreiben lässt und in einer Klasse Eigenschaften und Methoden definiert sind, dann muss es auch ein Extrem geben, das nur Verhaltensweisen festlegt: Genau diese Position nehmen die Schnittstellen ein.
Die Aufgabe der Schnittstellen geht über die einfache Fähigkeit, Verhaltensweisen bereitzustellen, hinaus. Bekanntlich unterstützt die Common Language Runtime keine Mehrfachvererbung. Damit sind die .NET-Architekten möglichen Schwierigkeiten aus dem Weg gegangen, die mit der Mehrfachvererbung verbunden sind. Mehrfachvererbung ist nur schwer zu realisieren und wird deshalb in der Praxis auch nur selten eingesetzt. Andererseits hielt man es als für erstrebenswert, neben der Basisklasse weitere »Oberbegriffe« zuzulassen, um gemeinsame Merkmale mehrerer ansonsten unabhängiger Klassen beschreiben zu können. Mit der Schnittstelle wurde ein Konstrukt geschaffen, das genau diese Möglichkeiten bietet.
Sie müssen sich Schnittstellen wie eine Vertragsvereinbarung vorstellen. Sobald eine Klasse eine Schnittstelle implementiert, haben alle auf die Klasse zugreifenden Clients die Garantie, dass die Klasse alle Verhaltensdefinitionen der Schnittstelle veröffentlicht. Mit anderen Worten: Eine Schnittstelle legt einen Vertragsrahmen fest, den die implementierende Klasse erfüllen muss.
4.1.1 Schnittstellendefinition 

Schnittstellen können Methoden, Eigenschaften und Indexer implementieren (Indexer werden im nächsten Kapitel behandelt). Das Besondere an einer Schnittstelle ist, dass sie selbst keine Codeimplementierung enthält, sondern ausnahmslos nur abstrakte Definitionen. Schauen wir uns dazu ein einfaches Beispiel an:
public interface ILuftfahrzeug { string Hersteller {get; set;} void Starten(); void Landen(); }
Die Definition einer Schnittstelle ähnelt der Definition einer Klasse, bei der das Schlüsselwort class gegen das Schlüsselwort interface ausgetauscht worden ist. Fehlt die Angabe eines Zugriffsmodifizierers, gilt eine Schnittstelle standardmäßig als internal, ansonsten bietet sich noch public an. Hinter der Definition werden in geschweiften Klammern alle Mitglieder der Schnittstelle aufgeführt, die durch ein Semikolon voneinander getrennt werden. Beachten Sie, dass das von den abstrakten Klassen her bekannte Schlüsselwort abstract in einer Schnittstellendefinition nicht auftaucht.
Konventionsgemäß wird dem Bezeichner einer Schnittstelle ein »I« vorangestellt.
Die Schnittstelle ILuftfahrzeug implementiert die Eigenschaft Hersteller sowie die Methoden Starten und Landen. Weil eine Schnittstelle grundsätzlich nur abstrakte Definitionen bereitstellt, enthält kein Mitglied einen Anweisungsblock. Es ist auch kein Zugriffsmodifizierer angegeben, weil eine Schnittstellendefinition das nicht erlaubt. Der Compiler würde mit einer Fehlermeldung reagieren, wenn Sie einem Schnittstellenmitglied einen Zugriffsmodifizierer voranstellen, denn die Festlegung eines Zugriffsmodifizierers ist die Aufgabe der implementierenden Klasse.
Schnittstellen definieren oft nur einen einzelnen Member, seltener eine Gruppe zusammengehöriger Member. Damit wird eine Spezifikation beschrieben, an die sich die implementierende Klasse halten muss: Sie verpflichtet sich, alle Elemente der Schnittstelle zu übernehmen. Auf eine Schnittstelle sind Sie bereits in Kapitel 3 gestoßen: Es war IDisposable. Klassen, die dieses Interface implementieren, garantieren damit, eine Methode Dispose bereitzustellen.
4.1.2 Schnittstellenimplementierung 

Bei der Vererbung wird von »Ableitung« gesprochen, analog wurde bei den Schnittstellen der Begriff »Implementierung« geprägt. Eine Schnittstelle ist wie ein Vertrag, den eine Klasse unterschreibt, sobald sie eine bestimmte Schnittstelle implementiert. Das hat Konsequenzen: Eine Klasse, die eine Schnittstelle implementiert, muss ausnahmslos jedes Mitglied der Schnittstelle übernehmen.
Eine zu implementierende Schnittstelle wird, getrennt durch einen Doppelpunkt, hinter dem Klassenbezeichner angegeben:
class Hubschrauber : ILuftfahrzeug {...}Eine Klasse ist nicht nur auf die Implementierung einer Schnittstelle beschränkt, es dürfen – im Gegensatz zur Vererbung – auch mehrere sein. Wird die Klasse außerdem noch aus einer anderen Klasse abgeleitet oder implementiert die Klasse mehrere Schnittstellen, werden alle Typbezeichner durch Kommata getrennt aufgelistet:
class Hubschrauber : ILuftfahrzeug, IDisposable {...}Schnittstellen dürfen nach der Veröffentlichung nicht mehr verändert werden, da sowohl der Client als auch die implementierende Klasse in einem Vertragsverhältnis zueinander stehen und die Bedingungen des Vertrags erfüllt werden müssen. Im wirklichen Leben ist das auch nicht anders. Mit der Veröffentlichung einer Schnittstelle erklärt sich eine Klasse bereit, die Schnittstelle exakt so zu implementieren, wie sie entworfen wurde. Die von der Klasse übernommenen Mitglieder der Schnittstelle müssen daher in jeder Hinsicht identisch zu ihrer Definition sein:
- Der Name muss dem Namen in der Schnittstelle entsprechen.
- Der Rückgabewert und die Parameterliste dürfen nicht von denen in der Schnittstellendefinition abweichen.
Ein aus einer Schnittstelle übernommenes Mitglied darf nur public sein. Es ist zulässig, in einer Klasse ein Schnittstellenmitglied abstract oder virtual zu implementieren, es darf jedoch nicht static oder const sein.
Das folgende Codefragment zeigt die Klasse Hubschrauber, die die oben definierte Schnittstelle ILuftfahrzeug implementiert und konventionsgemäß alle Schnittstellenmitglieder veröffentlicht:
public class Hubschrauber : ILuftfahrzeug { public void Starten() { // Anweisungen } public void Landen() { // Anweisungen } public string Hersteller { get { // Anweisungen } set { // Anweisungen } } }
Schnittstellen und Vererbung
Vererbung ist eines der Kernkonzepte objektorientierter Systeme. Eine Klasse, die aus einer anderen Klasse abgeleitet wird, erbt alle Methoden der Basisklasse. Wir wissen auch, dass Ereignisse aus der Basisklasse nicht an die abgeleitete Klasse vererbt werden. Daher stellt sich die Frage, ob ein Interface gleichermaßen ein nicht vererbbares Feature darstellt oder ob die abgeleitete Klasse die aus einer Schnittstelle übernommenen Methoden der Basisklasse veröffentlicht. Wir wollen das an einem kleinen Beispiel prüfen.
interface ILuftfahrzeug { void Starten(); } class Flugzeug : ILuftfahrzeug { public void Starten() { Console.WriteLine("Das Flugzeug startet"); } } class Motorflugzeug : Flugzeug { // Anweisungen } class Program { static void Main(string[] args) { Motorflugzeug flg = new Motorflugzeug (); flg.Starten(); Console.ReadLine(); } }
Die Schnittstelle ILuftfahrzeug definiert die Methode Starten, die von jeder implementierenden Klasse übernommen werden muss – im Code oben ist es die Klasse Flugzeug. Motorflugzeug wird aus Flugzeug abgeleitet. Wenn Sie dieses sehr einfache Programm starten, wird es fehlerfrei ausgeführt und beweist damit, dass auch die Schnittstellenmethoden vererbt werden.
Hinsichtlich einer Schnittstelle zeigt eine Methode polymorphes Verhalten. Das setzt sich jedoch nicht bei den ableitenden Klassen durch. Eine ableitende Klasse kann daher die implementierte Methode nur erben oder mit new verdecken. Soll die Methode in der schnittstellenimplementierenden Klasse den ableitenden Klassen polymorph angeboten werden, muss sie mit virtual signiert werden.
Mehrdeutigkeiten mit expliziter Implementierung vermeiden
Wenn eine Klasse mehrere Schnittstellen implementiert, kann es passieren, dass in zwei oder mehr Schnittstellen ein gleichnamiges Mitglied definiert ist. Diese Mehrdeutigkeit wird durch die explizite Implementierung eines Schnittstellenmembers aus der Welt geschafft. Eine explizite Implementierung ist der vollständig kennzeichnende Name eines Schnittstellenmitglieds. Er besteht aus dem Namen der Schnittstelle und dem Bezeichner des implementierten Mitglieds, getrennt durch einen Punkt.
Nehmen wir an, in den beiden Schnittstellen ICopy und IDocument wäre jeweils eine Methode Copy definiert:
// Interface-Definitionen public interface ICopy { void Copy(); } public interface IDocument { void Copy(); }
In einer Klasse ClassA, die beide Schnittstellen veröffentlicht, könnten die Methoden folgendermaßen explizit implementiert werden, um sie eindeutig den Schnittstellen zuzuordnen:
Class ClassA : ICopy, IDocument{ void ICopy.Copy() { Console.WriteLine("Copy-Methode in ICopy"); } void IDocument.Copy() { Console.WriteLine("Copy-Methode in IDocument"); } }
Es müssen nicht zwangsläufig beide Copy-Methoden explizit implementiert werden. Um eine eindeutige Schnittstellenzuordnung zu gewährleisten, würde eine explizite Implementierung vollkommen ausreichen. Explizit implementierte Methoden haben keinen Zugriffsmodifizierer, denn im Zusammenhang mit der expliziten Schnittstellenimplementierung ist eine wichtige Regel zu beachten:
Bei der expliziten Implementierung eines Schnittstellenmembers darf weder ein Zugriffsmodifizierer noch einer der Modifikatoren abstract, virtual, override oder static angegeben werden.
Auf die explizite Implementierung eines Schnittstellenmembers kann nur über eine Schnittstelleninstanz zugegriffen werden, der die Referenz auf das konkrete Objekt zugewiesen wird, dessen Typdefinition das Schnittstellenmember explizit implementiert.
class ClassA : ICopy, IDocument { static void Main(string[] args) { ClassA obj = new ClassA(); ICopy myCopy = obj; myCopy.Copy(); IDocument myDocu = obj; myDocu.Copy(); } void ICopy.Copy() { Console.WriteLine("ICopy.Copy in ClassA"); } void IDocument.Copy() { Console.WriteLine("IDocument.Copy in ClassA"); } }
Einer Objektvariablen kann nur dann eine Referenz zugewiesen werden, wenn sich beide in einer Vererbungsbeziehung befinden, sich eine Klasse somit aus einer anderen ableitet. Eine implementierte Schnittstelle wird wie eine Basisklasse behandelt. Deshalb kann einer Schnittstellenreferenz die Referenz auf ein Objekt vom Typ der implementierenden Klasse zugewiesen werden. Es kommt zu einer impliziten Konvertierung:
ICopy myCopy = obj;
Im Anschluss daran wird auf die Schnittstellenreferenz die Methode Copy aufgerufen:
myCopy.Copy();
Eine in einer Schnittstelle definierte Methode ist, wie auch eine abstrakte Methode, implizit virtuell. Die Bindung erfolgt dynamisch zur Laufzeit, der Aufruf ist polymorph und wird an die Methode des tatsächlichen Typs weitergeleitet. An der Konsole erscheint folgende Ausgabe:
ICopy.Copy in ClassA IDocument.Copy in ClassA
Explizite Implementierungen von Schnittstellenmitgliedern heben Mehrdeutigkeiten bei Schnittstellenmitgliedern mit derselben Signatur auf. Ohne explizite Implementierung könnte eine Klasse keine gleichnamigen Schnittstellenmethoden mit derselben Signatur haben.
Schnittstellen, die selbst Schnittstellen implementieren
Mehrere Schnittstellen können zu einer neuen Schnittstelle zusammengefasst werden. Das folgende Codefragment zeigt, wie die Schnittstelle ILuftfahrzeug die beiden Schnittstellen IStarten und ILanden beerbt.
interface IStarten { void Starten(); } interface ILanden : IStarten { void Landen(); } interface ILuftfahrzeug : ILanden, IStarten { void Fliegen(); }
Eine Klasse, die sich die Dienste der Schnittstellen IStarten, ILanden und ILuftfahrzeug sichern möchte, braucht dazu nur die Schnittstelle ILuftfahrzeug zu implementieren:
class Flugzeug : ILuftfahrzeug { public void Fliegen() { // Anweisungen } public void Starten() { // Anweisungen } public void Landen() { // Anweisungen } }
Hat eine Klasse eine bestimmte Schnittstelle implementiert?
In der täglichen Programmierpraxis werden Sie immer wieder auf dieselben Schwierigkeiten stoßen und Lösungen entwerfen müssen. Eine der aufgeworfenen Fragen wird lauten: Wie kann ich feststellen, ob der Typ eines Objekts eine bestimmte Schnittstelle implementiert?
Betrachten wir dazu ein einfaches Beispiel, und stellen wir uns vor, dass in der Anwendung CircleApplication die Schnittstelle IDraw definiert ist, die von den Klassen GraphicCircle und GraphicRectangle implementiert wird.
// ------------------------------------------------------------// Beispiel: ...\Kapitel 4\Schnittstellenprüfung
// ----------------------------------------------------------------
public interface IDraw { void Draw(); } public class GeometricObject {/*...*/} public class Circle : GeometricObject {/*...*/} public class GraphicCircle : Circle, IDraw { public void Draw() { Console.WriteLine("Der Kreis wird gezeichnet"); } } public class Rectangle : GeometricObject {/*...*/} public class GraphicRectangle : Rectangle, IDraw { public void Draw() { Console.WriteLine("Das Rechteck wird gezeichnet"); } }
Nun soll ein Array Objekte vom Typ Circle, GraphicCircle, Rectangle und GraphicRectangle verwalten. Die Methode Draw der Schnittstelle IDraw soll auf alle Objekte ausgeführt werden, die diese Schnittstelle veröffentlichen. Dabei kann es sich nur um die Objekte vom Typ GraphicCircle und GraphicRectangle handeln.
Zur Vermeidung von Fehlern muss in einer Schleife jede Objektreferenz zuerst daraufhin geprüft werden, ob der von ihr beschriebene Typ die Schnittstelle IDraw implementiert. Der Typoperator is, der im Kontext der if-Anweisung eingesetzt wird, erfüllt diese Aufgabe:
if(<Objektreferenz> is <Schnittstelle>) ...
Wir haben diesen Operator bereits im Zusammenhang mit der Typüberprüfung eingesetzt.
In der folgenden Main-Methode wird ein Array mit zehn Elementen vom Typ der allen gemeinsamen Basisklasse GeometricObject deklariert. In einer Schleife wird jedem Array-Element die Referenz auf ein Objekt vom Typ Circle, GraphicCircle, Rectangle oder GraphicRectangle zugewiesen. Die überladene Methode Next der Klasse Random, die uns mit den beiden Argumenten 0 und 4 die Zufallszahlen 0, 1, 2 oder 3 liefert, bestimmt mit dem jeweiligen Rückgabewert für jedes Element den entsprechenden Typ. Ist der Rückgabewert eine 0, wird die Klasse Circle instanziiert, mit 1 die Klasse GraphicCircle usw.
class Program { static void Main(string[] args) { GeometricObject[] obj = new GeometricObject[10]; Random rnd = new Random(); int zufall; // Array-Elemente initialisieren for(int i = 0; i < obj.Length; i++) { zufall = rnd.Next(0, 4); switch(zufall) { case 0: obj[i] = new Circle(); break; case 1: obj[i] = new GraphicCircle(); break; case 2: obj[i] = new Rectangle(); break; case 3: obj[i] = new GraphicRectangle(); break; } } // Ausgabe an der Konsole for(int i = 0; i < obj.Length; i++) { if(obj[i] is IDraw) { Console.WriteLine(obj[i].GetType()); ((IDraw)obj[i]).Draw(); Console.WriteLine("----------------------------"); } } Console.ReadLine(); } }
Interessant ist für uns insbesondere die zweite Schleife, in der mit
if(obj[i] is IDraw)
geprüft wird, ob der Typ der aktuellen Referenz die Schnittstelle IDraw implementiert. Im positiven Fall wird das Array-Element zuerst explizit in den Typ der Schnittstelle konvertiert und darauf die Methode Draw polymorph ausgeführt:
((IDraw)obj[i]).Draw();
Die Ausgabe könnte beispielsweise wie folgt lauten:
CircleApplication.GraphicCircle Der Kreis wird gezeichnet ---------------------------------------------
CircleApplication.GraphicRectangle Das Rechteck wird gezeichnet ---------------------------------------------
...
Wie Sie sehen, spielt auch hier die Polymorphie eine tragende Rolle, um die zu einem bestimmten Objekt gehörende Methode aufzurufen.
4.1.3 Typumwandlung mit dem »as«-Operator 

Ein Objekt kann mit dem ()-Konvertierungsoperator in eine kompatible Schnittstelle umgewandelt werden. Beispielsweise kann eine Instanz der Klasse GraphicCircle in die Schnittstelle IDraw konvertiert und dann einer Variablen dieses Typs zugewiesen werden:
IDraw id = (IDraw)obj;
C# bietet mit dem as-Operator noch eine weitere Konvertierungsvariante an:
IDraw id = obj as IDraw;
Das Ergebnis ist dasselbe – wenn die angegebene Instanz auch wirklich die Schnittstelle implementiert. Beide Möglichkeiten verhalten sich aber unterschiedlich, wenn das Objekt die Schnittstelle nicht implementiert:
- Die Konvertierung löst eine Exception (Ausnahme) aus, wenn das Objekt die Schnittstelle nicht implementiert.
- Der as-Operator liefert als Ergebnis null, wenn das Objekt die Schnittstelle nicht implementiert.
Der as-Operator bietet sich deshalb insbesondere dann an, wenn in einem if-Statement eine Schnittstellenuntersuchung vorgenommen werden soll:
if(obj as IDraw != null) ...
Beachten Sie, dass der as-Operator nur im Zusammenhang mit Referenztypen genutzt werden kann.
4.1.4 Abstrakte Klassen vs. Schnittstellen 

Die Notwendigkeit bzw. der Vorteil der Schnittstellenimplementierung ist am Anfang nicht einfach zu verstehen. Bestimmt werden Sie erkannt haben, dass die Schnittstellenimplementierung als eine Ergänzung der Implementierungsvererbung angesehen werden kann. Die Beispiele waren bisher alle sehr einfach gehalten, nun aber wollen wir uns einem komplexeren Beispiel zuwenden und dabei die Lösung für ein Problem erarbeiten, das zuerst durch eine abstrakte Klasse beschrieben wird und im zweiten Ansatz durch eine Schnittstelle.
Problembeschreibung
Stellen Sie sich vor, Sie möchten einen Algorithmus implementieren, der die Objekte eines Arrays der Größe nach auf- bzw. absteigend sortiert. Der Algorithmus soll sich dabei nur auf bestimmte .NET-Typen beschränken. Eine leichte Aufgabe, werden Sie jetzt vermutlich denken. Aber aus der Einschränkung auf bestimmte Typen resultiert eine verhältnismäßig komplizierte Lösung.
Die Sortierroutine
Am Anfang steht die Überlegung, die Sortierroutine mit dem Bezeichner SortElements in einer eigenen Klasse zu implementieren. Der erste Ansatz könnte dann wie folgt aussehen:
public class ArraySort { public static void SortElements(IrgendEinTyp[] arr) { // Anweisungen } }
Der Rückgabewert von SortElements ist void, folglich wird das sortierte Array über den Parameter an den Aufrufer zurückgegeben. Der Implementierung werden wir uns gleich widmen, denn zuerst müssen wir uns Gedanken darüber machen, von welchem Typ das übergebene Array sein soll. Im Codefragment ist der Typ noch mit IrgendEinTyp angegeben.
Um die Sortierung auf bestimmte Typen einzuschränken, müssen wir die Typen exakt festlegen. Dazu definieren wir eine zweite Klasse, die später als Basisklasse von den Klassen abgeleitet werden muss, deren Instanzen von SortElements sortiert werden sollen. Wir legen damit den Typ des übergebenen Arrays fest, denn nach den Regeln der Objektorientierung gilt, dass ein Objekt einer abgeleiteten Klasse auch gleichzeitig vom Typ seiner Basisklasse ist.
public class SortableObject { // Anweisungen }
Nun können wir den Methodenkopf von SortElements anpassen:
public static void SortElements(SortableObject[] arr)
Damit genügen wir der Forderung, nur bestimmte .NET-Typen sortieren zu können. Unabhängig davon, ob ein Array vom Typ Person, Elefant oder ClassA übergeben wird, wird der Parameter das Array mittels impliziter Konvertierung in Empfang nehmen – vorausgesetzt natürlich, dass die Klassen von SortableObject abgeleitet sind. Widmen wir uns nun der Realisierung der Methode SortElements. Es gibt verschiedene Algorithmen, um Elemente zu sortieren: Bubblesort, Quicksort, Insertionsort – um nur einige zu nennen. Die Bevorzugung eines dieser Sortierverfahren hängt vom Umfang der Daten und vom durchzuführenden Vergleich ab.
Für unser Beispiel habe ich mich für das Bubblesort-Verfahren entschieden. Der Name rührt wohl daher, dass sich die Funktionsweise sehr gut mit den aufsteigenden Luftblasen in einer Flüssigkeit vergleichen lässt. Die Elemente eines Arrays werden in aufsteigender Richtung durchlaufen, und dabei werden immer zwei benachbarte Elemente verglichen. Angenommen, ein Array namens MyArr mit vier Elementen soll der Größe nach sortiert werden, dann würden nacheinander die Elementpaare
MyArr[0] - MyArr[1] MyArr[1] - MyArr[2] MyArr[2] - MyArr[3]
verglichen werden, und jedes Paar würde in die richtige Reihenfolge gebracht. Wenn das Array aufsteigend sortiert werden soll, muss das zweite Element größer als das erste sein. Die Folge ist nach diesen drei Vergleichen, dass das höchstwertige Element – selbst wenn es sich im ursprünglichen Array ganz am Anfang befindet – bis an das Ende des Arrays (MyArr[3]) durchgereicht worden ist. Die Anzahl der Paarvergleiche entspricht der Bedingung:
Anzahl der Array-Elemente - 1
Dieser Durchlauf wird wiederholt, wobei das bereits richtig einsortierte Element keine Berücksichtigung mehr findet:
MyArr[0] - MyArr[1] MyArr[1] - MyArr[2]
Nach dem zweiten Durchlauf befindet sich das Element mit dem zweithöchsten Wert an der vorletzten Array-Position. Die Paarvergleiche werden so lange fortgesetzt, bis der Algorithmus mit dem letzten Paarvergleich
MyArr[0] - MyArr[1]
beendet wird.
Das Bubblesort-Sortierverfahren lässt sich am einfachsten mit 2 Schleifen wie folgt implementieren:
- mit einer äußeren Schleife mit einer Anzahl von Schleifendurchläufen, die der Bedingung Anzahl der Array-Elemente – 1 genügt.
- mit einer inneren Schleife, die den Paarvergleich durchführt und gegebenenfalls die Reihenfolge der benachbarten Elemente vertauscht.
Ausschlaggebend dafür, an welcher Position sich ein Objekt im sortierten Array einreiht, ist der paarweise Objektvergleich. Es stellt sich nun allerdings die Frage, nach welchen Kriterien Objekte vom Typ SortableObject verglichen werden sollen. Die statische Methode SortElements kann darüber keine Entscheidung treffen, da sie die typspezifischen Vergleichskriterien nicht kennt. Konsequenterweise muss der Vergleich in den von SortableObject abgeleiteten Klassen erfolgen. Dazu wird den ableitenden Klassen eine Methode vorgeschrieben, die als Ergebnis des Vergleichs zweier typgleicher Objekte einen booleschen Wert liefert. Wir nennen diese Methode CompareTo.
Damit jede Klasse, die SortableObject ableitet, die Methode CompareTo nach eigenen Maßstäben implementiert, wird CompareTo in der Klasse SortableObject abstrakt definiert. Damit sieht die endgültige Klassendefinition wie folgt aus:
public abstract class SortableObject {
public abstract bool CompareTo(SortableObject obj);
}Der Rückgabewert soll true sein, wenn das Objekt, auf dem die Methode CompareTo aufgerufen wird, größer ist als das Objekt, das dem Parameter übergeben wird. In allen anderen Fällen sei der Rückgabewert false. Wie und nach welchen Gesichtspunkten der Vergleich erfolgt, entscheidet die Klasse, die die abstrakte Methode CompareTo überschreibt. Natürlich können die booleschen Rückgabewerte auch vertauscht werden. Dann werden die Array-Elemente jedoch nicht auf-, sondern absteigend sortiert.
Mit diesen Vorgaben kann nun die Methode SortElements vollständig implementiert werden:
public class ArraySort { public static void SortElements(SortableObject[] arr) { // n = Anzahl der Elemente int n = arr.Length; // Temp = temporäre Variable SortableObject Temp; for(int i = n - 1; i >= 1; i--) { for(int k = 0; k <= i-1; k++) { if(arr[k].CompareTo(arr[k + 1])) { Temp = arr[k]; arr[k] = arr[k + 1]; arr[k + 1] = Temp; } } } } }
Resümieren wir an dieser Stelle, denn wir haben bereits alle Anforderungen erfüllt. Wir haben die abstrakte Klasse SortableObject entwickelt, die die Methode CompareTo bereitstellt, um zwei Objekte miteinander zu vergleichen. Per Definition kann die Methode CompareTo nur Objekte vergleichen, deren Klassen die abstrakte Klasse SortableObject ableiten.
In der Klasse ArraySort ist eine Methode implementiert, die in der Lage ist, ein Array von SortableObject-Objekten der Größe nach zu sortieren. Aber warum müssen es gerade Objekte dieses Typs sein, warum nicht andere, beliebige Objekte, von denen beispielsweise einfach zwei Längenmaße miteinander verglichen werden? Die Antwort liefert ein Blick in die Implementierung der Sortierroutine SortElements. Der Entwickler der Klasse ArraySort kannte die abstrakte Klasse SortableObject. Er wusste, dass Klassen, die die Klasse SortableObject ableiten, die Methode CompareTo bereitstellen. Auf diese Kenntnis wird in der Sortierroutine zurückgegriffen, wenn die CompareTo-Methode auf einem Objekt aufgerufen wird.
ArraySort und SortableObject seien in einer Klassenbibliothek implementiert. In Kapitel 6 werden wir uns der Bereitstellung von Klassenbibliotheken genauer widmen. An dieser Stelle sei schon erläuternd gesagt, dass eine Klassenbibliothek als DLL-Datei kompiliert wird. DLLs und die darin beschriebenen öffentlichen Typen (also beispielsweise Klassen) können von jedem beliebigen anderen Programm benutzt werden. Auch die .NET-Klassenbibliothek ist ein Verbund, der aus vielen DLL-Dateien besteht.
Die ableitende Klasse
Versetzen wir uns in die Lage eines Benutzers, der eine Klasse namens LngNumber entwickelt, die unter anderem ein Feld vom Typ long bereitstellt. Dieser Benutzer möchte sicherstellen, dass ein Objekt-Array vom Typ LngNumber der Größe nach sortiert werden kann.
Um sich die Mühe einer eigenen Implementierung zu sparen, recherchiert er in diversen Dokumentationen und stößt auf die Klasse ArraySort mit ihrer Methode SortElements, die die Lösung seines Problems darstellt. Der Dokumentation entnimmt unser fiktiver Benutzer, dass er die Klasse SortableObject ableiten und deren abstrakte Methode CompareTo implementieren muss. Das Ergebnis könnte wie folgt aussehen:
public class LngNumber : SortableObject { private long lngValue; // Konstruktor public LngNumber(long lng) { lngValue = lng; } // Eigenschaft public long Value { get {return lngValue;} set {lngValue = value;} } // überschriebene Instanzmethode public override bool CompareTo(SortableObject b) { if(this.lngValue < ((LngNumber)b).lngValue) return true; return false; } }
Die Testanwendung
Jetzt müssen wir nur noch unsere Überlegungen durch eine Testanwendung bestätigen:
// ----------------------------------------------------------// Beispiel: ...\Kapitel 4\Sortierroutine1
// --------------------------------------------------------------
class Program { static void Main(string[] args) { LngNumber[] arr = new LngNumber[5]; arr[0] = new LngNumber(8); arr[1] = new LngNumber(6); arr[2] = new LngNumber(34); arr[3] = new LngNumber(232); arr[4] = new LngNumber(2); // Aufruf der statischen Methode SortElements unter // Übergabe des Objekt-Arrays ArraySort.SortElements(arr); // Ausgabe an der Konsole for(int i = 0; i <= 4; i++) { Console.Write("Element[" + i + "]"); Console.WriteLine(" = " + arr[i].Value); } Console.ReadLine(); } }
Zunächst wird ein Array aus fünf Elementen vom Typ LngNumber deklariert, die im zweiten Schritt durch die Übergabe der Initialisierungswerte an den Konstruktor konkretisiert werden. Die Array-Elemente liegen zunächst in unsortierter Reihenfolge vor und werden mit der Anweisung
ArraySort.SortElements(arr);
in die richtige Reihenfolge gebracht. Die Ausgabe an der Konsole wird für die Elemente des Arrays arr lauten:
Element[0] = 2 Element[1] = 6 Element[2] = 8 Element[3] = 34 Element[4] = 232
Sie sehen, dass die Sortierroutine ihre Aufgabe einwandfrei erledigt. Wenigstens haben sich unsere Mühen gelohnt, wenn der Weg auch ein wenig steinig war.
Die Lösung mit einer Schnittstellendefinition
Der Code des Beispiels aus dem vorhergehenden Abschnitt funktioniert tadellos. Aber ihm haftet ein wesentliches Problem an, das sehr häufig auftritt, wenn abstrakte Basisklassen abgeleitet werden: .NET unterstützt keine Mehrfachvererbung, sondern erlaubt nur eine Basisklasse. Solange die Klasse LngNumber nicht aus einer anderen Basisklasse abgeleitet wird, ist der oben gezeigte Lösungsansatz akzeptabel. Sobald aber eine weitere Basisklasse ins Rampenlicht rückt, muss ein anderer Weg beschritten werden.
Genau an dieser Stelle greift das Konzept der Schnittstellen. Denn anstatt eine abstrakte Klasse zu beerben, werden die abstrakten Methoden über eine Schnittstelle offengelegt. Damit wird ein Großteil der Funktionalität der Mehrfachvererbung wiedererlangt, ohne dass man die damit verbundenen Nachteile in Kauf nehmen muss. Da eine Klasse beliebig viele Schnittstellen implementieren darf, kann sie auch um die unterschiedlichsten Verhaltensweisen erweitert werden.
Damit wird aus der abstrakten Klasse Sortable eine Schnittstellendefinition, wie sie im Folgenden dargestellt ist:
// ---------------------------------------------------------// Beispiel: ...\Kapitel 4\Sortierroutine2
// -------------------------------------------------------------
public interface ISortableObject {
bool CompareTo(ISortableObject a);
}Konventionsgemäß ergänzen wir den Schnittstellenbezeichner um das Präfix »I«. Die Änderung einer abstrakten Klasse in eine Schnittstelle wirkt sich weder auf die Definition der Klasse ArraySort noch auf die Definition der Methode SortElements aus. Allerdings müssen die Klasse LngNumber und die aus der Schnittstelle übernommenen Methoden an die Schnittstellenimplementierung angepasst werden. Während die Ableitung einer abstrakten Klasse das Überschreiben der abstrakten Methoden mit dem Schlüsselwort override erforderlich macht, ist dieses bei der Implementierung der Schnittstellenmember in der implementierenden Klasse nicht zulässig.
Der folgende Codeausschnitt gibt die notwendigen Änderungen wieder.
class LngNumber : ISortableObject { private long lngValue; // Konstruktor public LngNumber(long lng) { lngValue = lng; } public long Value { get {return lngValue;} set {lngValue = value;} } // aus der SortableObject-Schnittstelle übernommene Methode public bool CompareTo(ISortableObject b) { if(this.lngValue < ((LngNumber)b).lngValue) return true; return false; } }
Damit ist die ursprünglich abstrakte Klasse durch eine Schnittstelle ersetzt worden, und der Code wird in gleicher Weise zum Ziel führen. Nicht anzuzweifeln ist die durch die Schnittstellendefinition gewonnene Flexibilität im Vergleich zur abstrakten Klasse, da eine Schnittstelle das möglicherweise unumgängliche Ableiten einer Basisklasse nicht blockiert. Daher sollten Schnittstellen immer dann bevorzugt eingesetzt werden, wenn die Implementierungsvererbung nicht unbedingt notwendig ist.




Jetzt bestellen





