4 Vererbung, Polymorphie und Interfaces
4.1 Die Vererbung

Die objektorientierte Programmierung baut auf drei Säulen auf: Datenkapselung, Vererbung und Polymorphie. Viele Entwickler sprechen sogar von vier Säulen, weil sie die Klassendefinition mit einbeziehen. Über Letzteres lässt sich trefflich diskutieren, da eine Klassendefinition selbst wieder das Fundament der anderen drei Säulen ist. Aber wie dem auch sei, zwei Säulen bleiben für uns noch übrig, nämlich die Vererbung und die Polymorphie. Beiden wollen wir uns in diesem Kapitel widmen.
4.1.1 Basisklassen und abgeleitete Klassen

Welche Fähigkeit würden Sie von einem Circle-Objekt neben den bereits implementierten Fähigkeiten noch erwarten? Wahrscheinlich eine ganz wesentliche, nämlich die Fähigkeit, sich in einer beliebigen grafikfähigen Komponente visualisieren zu können. Bisher fehlt dazu noch eine passende Methode.
Die Klasse Circle soll jedoch von uns als abgeschlossen betrachtet werden. Damit simulieren wir zwei Ausgangssituationen, die in der täglichen Praxis häufig auftreten:
- Die Implementierung einer Klasse, wie beispielsweise Circle, ist für viele Anwendungsfälle völlig ausreichend. Eine Ergänzung der Memberliste würde nicht allgemeinen, sondern nur speziellen Zusatzanforderungen genügen.
- Die Klasse liegt im kompilierten Zustand vor. Damit besteht auch keine Möglichkeit, den Quellcode der Klasse um weitere Fähigkeiten zu ergänzen.
Wie kann das Problem gelöst werden, eine Klasse um zusätzliche Fähigkeiten zu erweitern, damit sie weiter gehenden Anforderungen gewachsen ist?
Die Antwort ist sehr einfach und lautet: Es muss eine weitere Klasse entwickelt werden. Diese soll im weiteren Verlauf GraphicCircle heißen. Die zusätzliche Klasse soll einerseits alle Fähigkeiten der Klasse Circle haben und darüber hinaus auch noch eine Methode namens Draw, um das Objekt zu zeichnen. Mit der Vererbung, einer der eingangs erwähnten Säulen der objektorientierten Programmierung, ist die Lösung sehr einfach zu realisieren.
Eine Klasse, die ihre Member als Erbgut einer abgeleiteten Klasse zur Verfügung stellt, wird als Basisklasse bezeichnet. Die erbende Klasse ist die Subklasse oder einfach nur die abgeleitete Klasse. Dem Grundprinzip der Vererbung folgend, verfügen abgeleitete Klassen normalerweise über mehr Funktionalitäten als ihre Basisklasse.
Zwei Klassen, die miteinander in einer Vererbungsbeziehung stehen, werden, wie in Abbildung 4.1 gezeigt, durch einen Beziehungspfeil von der abgeleiteten Klasse in Richtung der Basisklasse dargestellt.
Abbildung 4.1 Die Vererbungsbeziehung zwischen den Klassen »Circle« und »GraphicCircle«
Die Vererbungslinie ist nicht zwangsläufig mit dem Ableiten einer Klasse aus einer Basisklasse beendet. Eine Subklasse kann ihrerseits selbst zur Basisklasse mutieren, wenn sie selbst abgeleitet wird. Es ist auch möglich, von einer Klasse mehrere Subklassen abzuleiten, die dann untereinander beziehungslos sind. Am Ende kann dadurch eine nahezu beliebig tiefe und weit verzweigte Vererbungshierarchie entstehen, die einer Baumstruktur ähnelt.
Jeder Baum hat einen Stamm. Genauso sind auch alle Klassen von .NET auf eine allen gemeinsame Klasse zurückzuführen: Object. Diese Klasse ist die einzige in der .NET-Klassenbibliothek, die selbst keine Basisklasse hat. Geben Sie bei einer Klassendefinition keine Basisklasse explizit an, ist Object immer die direkte Basisklasse. Deshalb finden Sie in der IntelliSense-Hilfe auch immer die Methoden Equals, GetType, ToString und GetHashCode, die aus Object geerbt werden.
Prinzipiell wird in der Objektorientierung zwischen der Einfach- und der Mehrfachvererbung unterschieden. Bei der einfachen Vererbung hat eine Klasse nur eine direkte Basisklasse, bei der Mehrfachvererbung können es mehrere sein. Eine Klassenhierarchie, die auf Mehrfachvererbung basiert, ist komplex und kann unter Umständen zu unerwarteten Nebeneffekten führen. Um solchen Konflikten aus dem Weg zu gehen, wird die Mehrfachvererbung von .NET nicht unterstützt. Damit werden einerseits zwar bewusst Einschränkungen in Kauf genommen, die aber andererseits durch die Schnittstellen (interfaces) nahezu gleichwertig ersetzt werden. Das Thema der Interfaces wird uns später in diesem Kapitel noch beschäftigen.
4.1.2 Die Ableitung einer Klasse

Wenden wir uns nun wieder unserem Beispiel zu, und ergänzen wir das Projekt GeometricObjectsSolution um die Klasse GraphicCircle, die die Klasse Circle ableiten soll. Zudem soll GraphicCircle um die typspezifische Methode Draw erweitert werden. Die Ableitung wird in der neuen Klassendefinition durch einen Doppelpunkt und die sich daran anschließende Bekanntgabe der Basisklasse zum Ausdruck gebracht:
public class GraphicCircle : Circle
{
public void Draw() {
Console.WriteLine("Der Kreis wird gezeichnet");
}
}
Listing 4.1 Die Definition der abgeleiteten Klasse »GraphicCircle«
Wir wollen an dieser Stelle das Kreisobjekt nicht wirklich zeichnen, sondern nur stellvertretend eine Zeichenfolge an der Konsole ausgeben.
Die Konsequenz der Vererbung können Sie zu diesem Zeitpunkt bereits sehen, wenn Sie ein Objekt des Typs GraphicCircle mit
GraphicCircle gCircle = new GraphicCircle();
erzeugen und danach die Punktnotation auf den Objektverweis anwenden: In der IntelliSense-Hilfe werden neben der neuen Methode Draw alle öffentlichen Mitglieder der Klasse Circle angezeigt, obwohl diese in der abgeleiteten Klasse nicht definiert sind (siehe Abbildung 4.2). Natürlich fehlen auch nicht die aus Object geerbten Methoden, die ebenfalls über die »Zwischenstation« Circle zu Mitgliedern der Klasse GraphicCircle werden.
Abbildung 4.2 Die von der Klasse »Circle« geerbten Fähigkeiten
Die Tatsache, dass ein Objekt vom Typ GraphicCircle alle Komponenten der Klasse Circle offenlegt, lässt unweigerlich den Schluss zu, dass das Objekt einer abgeleiteten Klasse auch gleichzeitig ein Objekt der Basisklasse sein muss. Zwischen den beiden in der Vererbungshierarchie in Beziehung stehenden Klassen existiert eine Beziehung, die als Ist-ein(e)-Beziehung bezeichnet wird.
Ein Objekt vom Typ einer abgeleiteten Klasse ist gleichzeitig auch immer ein Objekt vom Typ seiner Basisklasse.
Das bedeutet konsequenterweise, dass ein Objekt vom Typ GraphicCircle gleichzeitig auch ein Objekt vom Typ Object ist – so wie auch ein Circle-Objekt vom Typ Object ist. Letztendlich ist alles im .NET Framework vom Typ Object. Eine weitere wichtige Schlussfolgerung kann ebenfalls daraus gezogen werden: In Richtung der Basisklassen werden die Objekte immer allgemeiner, in Richtung der abgeleiteten Klassen immer spezialisierter.
Die Aussage, dass es sich bei der Vererbung um die codierte Darstellung einer Ist-ein(e)-Beziehung handelt, sollten Sie sich sehr gut einprägen. Es hilft dabei, Vererbungshierarchien sinnvoll und realitätsnah umzusetzen. Sie werden dann sicher nicht auf die Idee kommen, aus einem Elefanten eine Mücke abzuleiten, nur weil der Elefant vier Beine hat und eine Mücke sechs. Sie würden in dem Sinne zwar aus einer Mücke einen Elefanten machen, aber eine Mücke ist nicht gleichzeitig ein Elefant ...
4.1.3 Klassen, die nicht abgeleitet werden können

Klassen, die als »sealed« definiert sind
Klassen, die abgeleitet werden, vererben den abgeleiteten Klassen ihre Eigenschaften und Methoden. Es kommt aber immer wieder vor, dass die weitere Ableitung einer Klasse keinen Sinn ergibt oder sogar strikt unterbunden werden muss, weil die von der Klasse zur Verfügung gestellten Dienste als endgültig betrachtet werden.
Um sicherzustellen, dass eine Klasse nicht weiter abgeleitet werden kann, wird die Klassendefinition um den Modifizierer sealed ergänzt:
public sealed class GraphicCircle {[...]}
Statische Klassen und Vererbung
Neben sealed-Klassen sind auch statische Klassen nicht vererbungsfähig. Darüber hinaus dürfen statische Klassen auch nicht aus einer beliebigen Klasse abgeleitet werden. Die einzig mögliche Basisklasse ist Object.
4.1.4 Konstruktoren in abgeleiteten Klassen

Bei der Erzeugung des Objekts einer abgeleiteten Klasse gelten dieselben Regeln wie beim Erzeugen des Objekts einer Basisklasse:
- Es wird generell ein Konstruktor aufgerufen.
- Der Subklassenkonstruktor darf überladen werden.
Konstruktoren werden grundsätzlich nicht von der Basisklasse an die Subklasse weitervererbt. Daher müssen alle erforderlichen bzw. gewünschten Konstruktoren in der abgeleiteten Klasse definiert werden. Das gilt auch für den statischen Initialisierer. Abgesehen vom impliziten, parameterlosen Standardkonstruktor
public GraphicCircle(){}
ist die Klasse GraphicCircle daher noch ohne weiteren Konstruktor. Um dem Anspruch zu genügen, einem Circle-Objekt auch hinsichtlich der Instanziierbarkeit gleichwertig zu sein, benötigen wir insgesamt drei Konstruktoren, die in der Lage sind, entweder den Radius oder den Radius samt den beiden Bezugspunktkoordinaten entgegenzunehmen. Außerdem müssen wir berücksichtigen, dass Objekte vom Typ GraphicCircle gleichzeitig Objekte vom Typ Circle sind. Die logische Konsequenz ist, den Objektzähler mit jedem neuen GraphicCircle-Objekt zu erhöhen. Mit diesen Vorgaben, die identisch mit denen in der Basisklasse sind, sieht der erste und, wie Sie noch sehen werden, etwas blauäugige und sogar naive Entwurf der Erstellungsroutinen in der Klasse GraphicCircle zunächst wie in Listing 4.2 gezeigt aus:
public class GraphicCircle : Circle {
public GraphicCircle() : this(0, 0, 0) { }
public GraphicCircle(int radius) : this(radius, 0, 0) { }
public GraphicCircle(int radius, double x, double y) {
Radius = radius;
XCoordinate = x;
YCoordinate = y;
Circle._CountCircles++;
}
}
Listing 4.2 Erste Idee der Konstruktorüberladung in »GraphicCircle«
Der Versuch, diesen Programmcode zu kompilieren, endet jedoch in einem Fiasko, denn der C#-Compiler kann das Feld _CountCircles nicht erkennen und verweigert deswegen die Kompilierung. Der Grund hierfür ist recht einfach: Das Feld ist in der Basisklasse Circle private deklariert. Private Member sind aber grundsätzlich nur in der Klasse sichtbar, in der sie deklariert sind. Obwohl aus objektorientierter Sicht ein Objekt vom Typ GraphicCircle auch gleichzeitig ein Objekt vom Typ Circle ist, kann die strikte Kapselung einer privaten Variablen durch die Vererbung nicht aufgebrochen werden. Nur der Code in der Klasse Circle hat Zugriff auf die in dieser Klasse definierten privaten Klassenmitglieder.
4.1.5 Der Zugriffsmodifizierer »protected«

Einen Ausweg aus diesem Dilemma, ein Klassenmitglied einerseits gegen den unbefugten Zugriff von außen zu schützen, es aber andererseits in einer abgeleiteten Klasse sichtbar zu machen, bietet der Zugriffsmodifizierer protected. Member, die als protected deklariert sind, verhalten sich ähnlich wie private deklarierte: Sie verhindern den unzulässigen Zugriff von außerhalb, garantieren jedoch andererseits, dass in einer abgeleiteten Klasse direkt darauf zugegriffen werden kann.
Diese Erkenntnis führt zu einem Umdenken bei der Implementierung einer Klasse: Muss davon ausgegangen werden, dass die Klasse als Basisklasse ihre Dienste zur Verfügung stellt, sind alle privaten Member, die einer abgeleiteten Klasse zur Verfügung stehen sollen, protected zu deklarieren. Daher müssen (oder besser »sollten« – siehe dazu auch die Anmerkung weiter unten) wir in der Klasse Circle noch folgende Änderungen vornehmen:
// Änderung der privaten Felder in der Klasse Circle
protected int _Radius;
protected static int _CountCircles;
Erst jetzt vererbt die Klasse Circle alle Member an die Ableitung GraphicCircle, und der C#-Compiler wird keinen Fehler mehr melden.
Selbstverständlich könnte man an dieser Stelle auch argumentieren, dass der Modifikator private eines Feldes aus der Überlegung heraus gesetzt worden ist, mögliche unzulässige Werte von vornherein zu unterbinden und – zumindest im Fall unseres Radius – den Zugang nur über get und set der Eigenschaftsmethode zu erzwingen. Andererseits kann man dem auch entgegenhalten, dass man bei der Bereitstellung einer ableitbaren Klasse nicht weiß, welche Intention hinter der Ableitung eine wichtige Rolle spielt. Mit dieser Argumentation ist eine »Aufweichung« des gekapselten Zugriffs durch protected durchaus vertretbar. In einer so geführten Diskussion muss dann aber auch noch ein weiterer Gesichtspunkt angeführt werden: Die Eigenschaftsmethode kann in einer ableitbaren Klasse auch neu implementiert werden. Darauf kommen wir später in diesem Kapitel noch zu sprechen.
Was also ist zu tun? private oder protected? Eine allgemeingültige Antwort gibt es nicht. Im Einzelfall müssen Sie selbst entscheiden, welchen Zugriffsmodifikator Sie für das Feld benutzen. Einfacher gestaltet sich die Diskussion nur hinsichtlich der Methoden. Wenn Sie den Zugriff aus einer abgeleiteten Klasse heraus auf eine Methode nicht wünschen, muss sie private definiert werden, ansonsten protected.
4.1.6 Die Konstruktorverkettung in der Vererbung

Wir wollen nun die Implementierung in Main testen, indem wir ein Objekt des Typs GraphicCircle erzeugen und uns den Stand des Objektzählers, der von der Circle-Klasse geerbt wird, an der Konsole ausgeben lassen. Der Code dazu lautet:
static void Main(string[] args) {
GraphicCircle gc = new GraphicCircle();
Console.WriteLine("Anzahl der Kreise = {0}", GraphicCircle.CountCircles);
}
Listing 4.3 Testen der Konstruktoren von »GraphicCircle« mit unerwartetem Resultat
Völlig unerwartet werden wir mit folgender Situation konfrontiert: Mit
Anzahl der Kreise = 2
wird uns suggeriert, wir hätten zwei Kreisobjekte erzeugt, obwohl wir doch tatsächlich nur einmal den new-Operator benutzt haben und sich folgerichtig auch nur ein konkretes Objekt im Speicher befinden kann.
Das Ergebnis ist falsch und beruht auf der bisher noch nicht berücksichtigten Aufrufverkettung zwischen den Sub- und Basisklassenkonstruktoren. Konstruktoren werden bekanntlich nicht vererbt und müssen deshalb – falls erforderlich – in jeder abgeleiteten Klasse neu definiert werden. Dennoch kommt den Konstruktoren der Basisklasse eine entscheidende Bedeutung zu. Bei der Initialisierung eines Subklassenobjekts wird nämlich in jedem Fall zuerst ein Basisklassenkonstruktor aufgerufen. Es kommt zu einer Top-down-Verkettung der Konstruktoren, angefangen bei der obersten Basisklasse (Object) bis hinunter zu der Klasse, deren Konstruktor aufgerufen wurde (siehe Abbildung 4.3).
Die Verkettung der Konstruktoraufrufe dient dazu, zunächst die geerbten Komponenten der Basisklasse zu initialisieren. Erst danach wird der Konstruktor der direkten Subklasse ausgeführt, der eigene Initialisierungen vornehmen kann und gegebenenfalls auch die Vorinitialisierung der geerbten Komponenten an die spezifischen Bedürfnisse der abgeleiteten Klasse anpasst. Standardmäßig wird dabei immer zuerst der parameterlose Konstruktor der Basisklasse aufgerufen.
Abbildung 4.3 Die Verkettung der Konstruktoraufrufe in einer Vererbungshierarchie
Die Konstruktorverkettung hat maßgeblichen Einfluss auf die Modellierung einer Klasse, die parametrisierte Konstruktoren enthält. Eine »konstruktorlose« Klasse hat grundsätzlich immer einen impliziten, parameterlosen Konstruktor. Ergänzt man jedoch eine Klasse um einen parametrisierten Konstruktor, existiert der implizite, parameterlose nicht mehr. Wird nun das Objekt einer abgeleiteten Klasse erzeugt, kommt es zum Aufruf des parameterlosen Konstruktors der Basisklasse. Wird dieser durch parametrisierte Konstruktoren überschrieben und nicht explizit codiert, meldet der Compiler einen Fehler.
Sie sollten sich dessen bewusst sein, wenn Sie eine ableitbare Klasse entwickeln und parametrisierte Konstruktoren hinzufügen. Das Problem ist sehr einfach zu lösen, indem Sie einen parameterlosen Konstruktor in der Basisklasse definieren.
Die Konstruktorverkettung mit »base« steuern
Nun erklärt sich auch das scheinbar unsinnige Ergebnis des Objektzählers im vorhergehenden Abschnitt, der bei der Instanziierung eines Objekts vom Typ GraphicCircle behauptete, zwei Kreisobjekte würden vorliegen, obwohl es nachweislich nur ein einziges sein konnte. Durch die Konstruktorverkettung wird zunächst der parameterlose Konstruktor der Basisklasse Circle aufgerufen, danach der der Klasse GraphicCircle. In beiden wird der Objektzähler erhöht, was letztendlich zu einem falschen Zählerstand führt. Die Ursache des Problems ist die Duplizität der Implementierung der beiden parameterlosen Konstruktoren, nämlich in Circle:
public Circle(...) {
[...]
Circle._CountCircles++;
}
und in der von Circle abgeleiteten Klasse GraphicCircle:
public GraphicCircle(...) {
[...]
Circle._CountCircles++;
}
Betrachten wir noch einmal die Implementierung der Konstruktoren in GraphicCircle: Alle Konstruktoraufrufe werden derzeit mit this an den Konstruktor mit den meisten Parametern weitergeleitet. Bei der Erzeugung eines GraphicCircle-Objekts wird zudem standardmäßig der parameterlose der Klasse Circle aufgerufen, der den Aufruf seinerseits intern an den dreifach parametrisierten in dieser Klasse weiterleitet. Außerdem entspricht der Code in den Konstruktoren von GraphicCircle exakt dem Code in den gleich parametrisierten Konstruktoren in Circle.
Optimal wäre es, anstelle des klassenintern weiterleitenden this-Aufrufs in GraphicCircle den Aufruf direkt an den gleich parametrisierten Konstruktor der Basisklasse Circle zu delegieren. Dabei müssten die dem Konstruktor übergebenen Argumente an den gleich parametrisierten Konstruktor der Basisklasse weitergeleitet werden.
C# bietet eine solche Möglichkeit mit dem Schlüsselwort base an. Mit base kann der Konstruktoraufruf einer Klasse an einen bestimmten Konstruktor der direkten Basisklasse umgeleitet werden. base wird dabei genauso wie this eingesetzt, das heißt, Sie können base Argumente übergeben, um einen bestimmten Konstruktor in der Basis anzusteuern.
Das objektorientierte Paradigma schreibt vor, dass aus einer abgeleiteten Klasse heraus mittels Aufrufverkettung zuerst immer ein Konstruktor der Basisklasse ausgeführt werden muss. Per Vorgabe ist das bekanntermaßen der parameterlose. Mit base können wir die implizite, standardmäßige Konstruktorverkettung durch eine explizite ersetzen und die Steuerung selbst übernehmen: Es kommt zu keinem weiteren impliziten Aufruf des parameterlosen Basisklassenkonstruktors mehr.
In unserem Beispiel der Klasse Circle bietet es sich sogar an, sofort den dreifach parametrisierten Konstruktor der Basis aufzurufen. Sehen wir uns nun die überarbeitete Fassung der GraphicCircle-Konstruktoren an:
public GraphicCircle : base(0, 0, 0) { }
public GraphicCircle(int radius) : base(radius, 0, 0) { }
public GraphicCircle(int radius, double x, double y) : base(radius, x, y){ }
Listing 4.4 Die endgültige Version der Konstruktoren in »GraphicCircle«
Schreiben wir jetzt eine Testroutine, z. B.:
GraphicCircle gCircle = new GraphicCircle();
Console.WriteLine("Anzahl der Kreise = {0}", GraphicCircle.CountCircles);
Jetzt wird die Ausgabe des Objektzählers tatsächlich den korrekten Stand wiedergeben.
Der Zugriff auf die Member der Basisklasse mit »base«
Mit base kann nicht nur die Konstruktorverkettung explizit gesteuert werden. Sie können dieses Schlüsselwort auch dazu benutzen, um innerhalb einer abgeleiteten Klasse auf Member der Basisklasse zuzugreifen, solange sie nicht private deklariert sind. Dabei gilt, dass die Methode der Basisklasse, auf die zugegriffen wird, durchaus eine von dieser Klasse selbst geerbte Methode sein kann, also aus Sicht der base-implementierenden Subklasse aus einer indirekten Basisklasse stammt, beispielsweise:
class BaseClass {
public void DoSomething() {
Console.WriteLine("In 'BaseClass.DoSomething()'");
}
}
class SubClass1 : BaseClass {}
class SubClass2 : SubClass1 {
public void BaseTest() {
base.DoSomething();
}
}
Listing 4.5 Methodenaufruf in der direkten Basisklasse
Ein umgeleiteter Aufruf an eine indirekte Basisklasse mit
// unzulässiger Aufruf
base.base.DoSomething();
ist nicht gestattet. Handelt es sich bei der über base aufgerufenen Methode um eine parametrisierte, müssen den Parametern die entsprechenden Argumente übergeben werden.
base ist eine implizite Referenz und als solche an eine konkrete Instanz gebunden. Das bedeutet konsequenterweise, dass dieses Schlüsselwort nicht zum Aufruf von statischen Methoden verwendet werden kann.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.