4.2 Der Problemfall geerbter Methoden
Um das objektorientierte Konzept zu erläutern, habe ich mich bisher meistens des Beispiels der beiden Klassen Circle und GraphicCircle bedient. Sie haben mit diesen beiden Klassen gelernt, wie die Struktur einer Klasse samt ihren Feldern, Methoden und Konstruktoren aufgebaut ist. Sie wissen nun auch, wie durch die Vererbung eine Klasse automatisch mit Fähigkeiten ausgestattet wird, die sie aus der Basisklasse erbt. Nun werden wir uns einer zweiten Klassenhierarchie zuwenden, um weitere Aspekte der Objektorientierung auf möglichst anschauliche Weise zu erklären.
Ausgangspunkt ist die Klasse Luftfahrzeug, die von den beiden Klassen Flugzeug und Hubschrauber beerbt wird. In der Klasse Luftfahrzeug sind die Felder definiert, die alle davon abgeleiteten Klassen gemeinsam aufweisen: Hersteller und Baujahr. Die Spannweite ist eine Eigenschaft, die nur ein Flugzeug hat, und ist daher in der Klasse Flugzeug implementiert. Ein Hubschrauber wiederum hat demgegenüber einen Rotordurchmesser. Da die abgeleiteten Typen starten können, ist die entsprechende Methode in der Basisklasse Luftfahrzeug implementiert.
Das nachfolgende Codefragment bildet die Situation ab. Dabei enthält die Methode Starten nur »symbolischen« Code.
public class Luftfahrzeug {
public string Hersteller {get; set;}
public int Baujahr {get; set;}
public void Starten() {
Console.WriteLine("Das Luftfahrzeug startet.");
}
}
public class Flugzeug : Luftfahrzeug {
public double Spannweite {get; set;}
}
public class Hubschrauber : Luftfahrzeug {
public double Rotor {get; set;}
}
Listing 4.6 Klassen der Hierarchie der Luftfahrzeuge
In Abbildung 4.4 sehen Sie die Zusammenhänge auf anschauliche Art.
Abbildung 4.4 Die Hierarchie der Luftfahrzeuge
Grundsätzlich scheint die Vererbungshierarchie den Anforderungen zu genügen, aber denken Sie einen Schritt weiter: Ist die Implementierung der Methode Starten in der Basisklasse Luftfahrzeug anforderungsgerecht? Denn im Grunde genommen startet ein Flugzeug anders als ein Hubschrauber – zumindest in den meisten Fällen. Ganz allgemein ausgedrückt stehen wir vor der folgenden Frage: Wie kann eine Methode in der Basisklasse implementiert werden, wenn sich das operative Verhalten in den Methoden der abgeleiteten Klassen unterscheidet? Einfach auf die Bereitstellung der Methode in der Basisklasse zu verzichten, ist definitiv keine Lösung. Denn unsere Absicht sei es, zu garantieren, dass jede abgeleitete Klasse die Methode – in unserem Fall Starten – bereitstellt.
Prinzipiell bieten sich drei Lösungsansätze an:
- Wir verdecken die geerbten Methoden der Basisklasse in der abgeleiteten Klasse mit dem Modifizierer new.
- Wir stellen in der Basisklasse abstrakte Methoden bereit, die von den erbenden Klassen überschrieben werden müssen.
- Wir stellen in der Basisklasse virtuelle Methoden bereit.
Nachfolgend wollen wir alle drei Alternativen genau untersuchen.
4.2.1 Geerbte Methoden mit »new« verdecken
Nehmen wir an, dass in der Basisklasse die Methode Starten wie folgt codiert ist:
public class Luftfahrzeug {
public void Starten() {
Console.WriteLine("Das Luftfahrzeug startet.");
}
}
Listing 4.7 Annahme: Implementierung der Methode »Starten« in der Basisklasse
In den beiden abgeleiteten Klassen soll die Methode Starten nunmehr eine typspezifische Implementierung aufweisen. Realisiert wird das durch eine Neuimplementierung der Methode in der abgeleiteten Klasse. Dabei muss die Methode mit dem Modifizierer new signiert werden, um deutlich zu machen, dass es sich um eine beabsichtigte Neuimplementierung handelt und nicht um einen unbeabsichtigten Fehler. Man spricht bei dieser Vorgehensweise auch vom Ausblenden oder Verdecken einer geerbten Methode.
Exemplarisch sei das an der Klasse Flugzeug gezeigt, gilt aber natürlich in gleicher Weise auch für den Typ Hubschrauber:
public class Flugzeug : Luftfahrzeug {
public new void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Listing 4.8 Verdecken der geerbten Methode mit dem Modifikator »new«
Vom Verdecken oder Ausblenden einer geerbten Basisklassenmethode wird gesprochen, wenn in der abgeleiteten Klasse eine Methode implementiert wird,
- die den gleichen Namen und
- eine identische Parameterliste
besitzt wie eine Methode in der Basisklasse, diese aber durch eine eigene Implementierung vollständig ersetzt. Das ist beispielsweise der Fall, wenn die Implementierung in der Basisklasse für Objekte vom Typ der abgeleiteten Klasse falsch ist oder generell anders sein muss. Entscheidend für das Verdecken einer geerbten Methode ist die Ergänzung der Methodendefinition in der Subklasse um den Modifizierer new.
Wird eine Basisklassenmethode in der abgeleiteten Klasse verdeckt, wird beim Aufruf der Methode auf Objekte vom Typ der Subklasse immer die verdeckende Version ausgeführt. Zum Testen in Main schreiben wir den folgenden Code:
Flugzeug flg = new Flugzeug();
flg.Starten();
Hubschrauber hubi = new Hubschrauber();
hubi.Starten();
Listing 4.9 Testen der Methode »Starten«
Im Befehlsfenster kommt es zu den Ausgaben »Das Flugzeug startet.« und »Der Hubschrauber startet.«.
Sie finden das Beispiel auf der Buch-DVD unter ..\Beispiele\Kapitel 4\Aircrafts\Sample1.
Statische Member überdecken
In gleicher Weise, wie eine geerbte Instanzmethode in einer ableitenden Klasse verdeckt werden kann, lassen sich mit new auch Eigenschaftsmethoden und statische Komponenten einer Basisklasse verdecken und durch eine typspezifische Implementierung ersetzen. Die in den folgenden Abschnitten noch zu behandelnden Modifizierer abstract, virtual und override sind im Zusammenhang mit statischen Membern nicht erlaubt.
4.2.2 Abstrakte Methoden
Mit dem Modifizierer new können die aus der Basisklasse geerbten Methoden in der ableitenden Klasse überdeckt werden. Allerdings ist dieser Lösungsweg mit einem Nachteil behaftet, denn er garantiert nicht, dass alle ableitenden Klassen die geerbte Methode Starten durch eine typspezifische Implementierung ersetzen. Jede unserer abgeleiteten Klassen sollte aber hinsichtlich der Behandlung einer Basisklassenoperation gleichwertig sein. Wird die Neuimplementierung beispielsweise in der Klasse Hubschrauber vergessen, ist dieser Typ mit einem möglicherweise entscheidenden Fehler behaftet, weil er keine typspezifische Neuimplementierung hat.
Wie können wir aber alle Typen, die die Klasse Luftfahrzeug ableiten, dazu zwingen, die Methode Starten neu zu implementieren? Gehen wir noch einen Schritt weiter und stellen wir uns die Frage, ob wir dann überhaupt noch Code in der Methode Starten der Klasse Luftfahrzeug benötigen. Anscheinend nicht! Dass wir die Methode in der Basisklasse definiert haben, liegt im Grunde genommen nur daran, dass wir die Methode Starten in allen ableitenden Klassen bereitstellen wollen.
Mit dieser Erkenntnis mag die Lösung der aufgezeigten Problematik im ersten Moment verblüffen: Tatsächlich wird Starten in der Basisklasse nicht implementiert – sie bleibt einfach ohne Programmcode. Damit wäre aber noch nicht sichergestellt, dass die ableitenden Klassen die geerbte »leere« Methode typspezifisch implementieren. Deshalb wird in solchen Fällen sogar auf den Anweisungsblock verzichtet, der durch die geschweiften Klammern beschrieben wird.
In der objektorientierten Programmierung werden Methoden, die keinen Anweisungsblock haben, als abstrakte Methoden bezeichnet. Neben den Methoden, die das Verhalten eines Typs beschreiben, können auch Eigenschaften abstrakt definiert werden.
Abstrakte Methoden werden durch die Angabe des abstract-Modifizierers in der Methodensignatur gekennzeichnet, am Beispiel unserer Methode Starten also durch:
public abstract void Starten();
Abstrakte Methoden enthalten niemals Code. Die Definition einer abstrakten Methode wird mit einem Semikolon direkt hinter der Parameterliste abgeschlossen, die geschweiften Klammern des Anweisungsblocks entfallen.
Welchen Stellenwert nimmt aber eine Klasse ein, die eine Methode veröffentlicht, die keinerlei Verhalten aufweist? Die Antwort ist verblüffend einfach: Eine solche Klasse kann nicht instanziiert werden – sie rechtfertigt ihre Existenz einzig und allein dadurch, den abgeleiteten Klassen als Methodenbereitsteller zu dienen. Damit wird das Prinzip der objektorientierten Programmierung, gemeinsame Verhaltensweisen auf eine höhere Ebene auszulagern, nahezu auf die Spitze getrieben.
Eine nicht instanziierbare Klasse, die mindestens einen durch abstract gekennzeichneten Member enthält, ist ihrerseits selbst abstrakt und wird deshalb als abstrakte Klasse bezeichnet. Abstrakte Klassen sind nur dann sinnvoll, wenn sie abgeleitet werden. Syntaktisch wird dieses Verhalten in C# durch die Ergänzung des Modifikators abstract in der Klassensignatur beschrieben:
public abstract class Luftfahrzeug {
public abstract void Starten();
[...]
}
Listing 4.10 Abstrakte Definition der Methode »Starten«
Neben abstrakten Methoden darf eine abstrakte Klasse auch vollständig implementierte, also nichtabstrakte Methoden und Eigenschaften bereitstellen.
Die Signatur einer Methode und infolgedessen auch der dazugehörigen Klasse mit dem Modifizierer abstract kommt einer Forderung gleich: Alle nichtabstrakten Ableitungen einer abstrakten Klasse müssen die abstrakten Methoden der Basisklasse überschreiben. Wird in einer abgeleiteten Klasse das abstrakte Mitglied der Basisklasse nicht überschrieben, muss die abgeleitete Klasse ebenfalls abstract gekennzeichnet werden. Als Konsequenz dieser Aussagen bilden abstrakte Klassen das Gegenkonstrukt zu den Klassen, die mit sealed als nicht ableitbar gekennzeichnet sind. Daraus folgt auch, dass die Modifizierer sealed und abstract nicht nebeneinander verwendet werden dürfen.
Eine Klasse, die eine abstrakt definierte Methode enthält, muss ihrerseits selbst abstrakt sein. Der Umkehrschluss ist allerdings nicht richtig, denn eine abstrakte Klasse ist nicht zwangsläufig dadurch gekennzeichnet, mindestens ein abstraktes Mitglied zu enthalten. Eine Klasse kann auch dann abstrakt sein, wenn keiner ihrer Member abstrakt ist. Auf diese Weise wird eine Klasse nicht instanziierbar und das Ableiten dieser Klasse erzwungen.
abstract kann nur im Zusammenhang mit Instanzmembern benutzt werden. Statische Methoden können nicht abstrakt sein, deshalb ist das gleichzeitige Auftreten von static und abstract in einer Methodensignatur unzulässig.
Abstrakte Methoden überschreiben
Das folgende Codefragment beschreibt die Klasse Hubschrauber. In der Klassenimplementierung wird die abstrakte Methode Starten der Basisklasse überschrieben. Zur Kennzeichnung des Überschreibens einer abstrakten Basisklassenmethode dient der Modifizierer override:
class Hubschrauber : Luftfahrzeug {
public override void Starten() {
Console.WriteLine("Der Hubschrauber startet.");
}
}
Listing 4.11 Überschreiben der geerbten abstrakten Methode
Sollten Sie dieses Beispiel ausprobieren, müssen Sie Starten selbstverständlich auch in der Klasse Flugzeug mit override überschreiben.
Sie finden das komplette Beispiel auf der Buch-DVD unter ..\Beispiele\Kapitel 4\Aircrafts\ Sample2.
4.2.3 Virtuelle Methoden
Widmen wir uns nun der dritten anfangs aufgezeigten Variante, den virtuellen Methoden. Ausgangspunkt sei dabei folgender: Wir wollen Starten in der Basisklasse vollständig implementieren. Damit wären wir wieder am Ausgangspunkt angelangt mit einem kleinen Unterschied: Wir ergänzen die Methoden Starten mit dem Modifizierer virtual. Dann sieht die Klasse Luftfahrzeug wie folgt aus:
public class Luftfahrzeug {
public virtual void Starten() {
Console.WriteLine("Das Luftfahrzeug startet.");
}
}
Listing 4.12 Virtuelle Definition der Methode »Starten«
Nun ist die Methode virtuell in der Basisklasse definiert. Eine ableitende Klasse hat nun die Wahl zwischen drei Alternativen:
- Die ableitende Klasse erbt die Methode, ohne eine eigene, typspezifische Implementierung
vorzusehen, also:
public class Flugzeug : Luftfahrzeug { }
- Die ableitende Klasse verdeckt die geerbte Methode mit new, hier also:
public class Flugzeug : Luftfahrzeug {
public new void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
} - Die ableitende Klasse überschreibt die geerbte Methode mit override, also
public class Flugzeug : Luftfahrzeug {
public override void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Sie finden das komplette Beispiel auf der Buch-DVD unter ..\Beispiele\Kapitel 4\Aircrafts\ Sample3.
Sie werden sich an dieser Stelle wahrscheinlich fragen, worin sich die beiden letztgenannten Varianten unterscheiden. Diese Überlegung führt uns nach der Datenkapselung und der Vererbung zum dritten elementaren Konzept der Objektorientierung: der Polymorphie. Ehe wir uns aber mit der Polymorphie beschäftigen, müssen wir vorher noch die Typumwandlung in einer Vererbungshierarchie verstehen.
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.