4.4 Polymorphie
In Abschnitt 4.2 haben Sie erfahren, dass die Methode Starten in der Klasse Luftfahrzeug unterschiedlich bereitgestellt werden kann. Es ist nun an der Zeit, darauf einzugehen, welche Konsequenzen die drei Varianten haben.
Dazu schreiben wir in der Main-Methode zunächst Programmcode, mit dem abstrakt, virtuell und klassisch implementierte Methoden getestet werden sollen.
static void Main(string[] args) {
Luftfahrzeug[] arr = new Luftfahrzeug[4];
arr[0] = new Flugzeug();
arr[1] = new Hubschrauber();
arr[2] = new Hubschrauber();
arr[3] = new Flugzeug();
foreach(Luftfahrzeug temp in arr) {
temp.Starten();
}
Console.ReadLine();
}
Listing 4.15 Code, um die Methode »Starten« zu testen
Zuerst wird ein Array vom Typ Luftfahrzeug deklariert. Jedes Array-Element ist vom Typ Luftfahrzeug. Weil die Klassen Flugzeug und Hubschrauber von diesem Typ abgeleitet sind, kann jedem Array-Element nach der Regel der impliziten Konvertierung auch die Referenz auf ein Objekt vom Typ der beiden Subklassen zugewiesen werden:
arr[0] = new Flugzeug();
arr[1] = new Hubschrauber();
[...]
Danach wird innerhalb einer foreach-Schleife auf alle Array-Elemente die Methode Starten aufgerufen. Die Laufvariable ist vom Typ Luftfahrzeug, also vom Typ der Basisklasse. In der Schleife wird auf diese Referenz die Starten-Methode aufgerufen.
4.4.1 Die »klassische« Methodenimplementierung
Wir wollen an dieser Stelle zunächst die klassische Methodenimplementierung in der Basisklasse testen. Die beiden ableitenden Klassen sollen die geerbte Methode Starten mit dem Modifizierer new überdecken:
public class Luftfahrzeug {
public void Starten() {
Console.WriteLine("Das Luftfahrzeug startet.");
}
}
public class Flugzeug : Luftfahrzeug {
public new void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Listing 4.16 Testen der in der abgeleiteten Klasse überdeckenden Methode
Starten wir die Anwendung, wird die folgende Ausgabe viermal im Konsolenfenster angezeigt:
Das Luftfahrzeug startet.
Das Ergebnis ist zwar nicht spektakulär, hat aber weitreichende Konsequenzen. Wir müssen uns nämlich die Frage stellen, ob die Ausgabe das ist, was wir erreichen wollten. Vermutlich nicht, denn eigentlich sollte doch jeweils die typspezifische Methode Starten in der abgeleiteten Klasse ausgeführt werden.
Das ursächliche Problem ist das statische Binden des Methodenaufrufs an die Basisklasse. Statisches Binden heißt, dass die auszuführende Operation bereits zur Kompilierzeit festgelegt wird. Der Compiler stellt fest, von welchem Typ das Objekt ist, auf dem die Methode aufgerufen wird, und erzeugt den entsprechenden Code. Statisches Binden führt dazu, dass die Methode der Basisklasse aufgerufen wird, obwohl eigentlich die »neue« Methode in der abgeleiteten Klasse erforderlich wäre.
Das Beispiel macht deutlich, welchen Nebeneffekt das Überdecken einer Methode mit dem Modifizierer new haben kann: Der Compiler betrachtet das Objekt, als wäre es vom Typ der Basisklasse, und ruft die unter Umständen aus logischer Sicht sogar fehlerhafte Methode in der Basisklasse auf.
4.4.2 Abstrakte Methoden
Nun ändern wir den Programmcode in der Basisklasse Luftfahrzeug und stellen die Methode Starten als abstrakte Methode zur Verfügung. Die ableitenden Klassen erfüllen die Vertragsbedingung und überschreiben die geerbte Methode mit override. Am Programmcode in Main nehmen wir keine Änderungen vor.
public abstract class Luftfahrzeug {
public abstract void Starten();
}
public class Flugzeug : Luftfahrzeug {
public override void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Listing 4.17 Testen der überschreibenden Methode
Ein anschließender Start der Anwendung bringt ein ganz anderes Ergebnis als im ersten Versuch:
Das Flugzeug startet.
Der Hubschrauber startet.
Der Hubschrauber startet.
Das Flugzeug startet.
Tatsächlich werden nun die typspezifischen Methoden aufgerufen.
Anscheinend ist die Laufvariable temp der foreach-Schleife in der Lage, zu entscheiden, welche Methode anzuwenden ist. Dieses Verhalten unterscheidet sich gravierend von dem, was wir im Zusammenhang mit den mit new ausgestatteten, überdeckenden Methoden zuvor gesehen haben. Die Bindung des Methodenaufrufs kann nicht statisch sein, sie erfolgt dynamisch zur Laufzeit.
Die Fähigkeit, auf einer Basisklassenreferenz die typspezifische Methode aufzurufen, wird als Polymorphie bezeichnet und ist neben der Kapselung und der Vererbung die dritte Säule der objektorientierten Programmierung. Polymorphie bezeichnet ein Konzept der Objektorientierung, das besagt, dass Objekte bei gleichen Methodenaufrufen unterschiedlich reagieren können. Dabei können Objekte verschiedener Typen unter einem gemeinsamen Oberbegriff (d. h. einer gemeinsamen Basis) betrachtet werden. Die Polymorphie sorgt dafür, dass der Methodenaufruf automatisch bei der richtigen, also typspezifischen Methode landet.
Polymorphie arbeitet mit dynamischer Bindung. Der Aufrufcode wird nicht zur Kompilierzeit erzeugt, sondern erst zur Laufzeit der Anwendung, wenn die konkreten Typinformationen vorliegen. Im Gegensatz dazu legt die statische Bindung die auszuführende Operation wie gezeigt bereits zur Kompilierzeit fest.
4.4.3 Virtuelle Methoden
Überschreibt eine Methode eine geerbte abstrakte Methode, zeigt die überschreibende Methode ausnahmslos immer polymorphes Verhalten. Wird in einer Basisklasse eine Methode »klassisch« implementiert und in der ableitenden Klasse durch eine Neuimplementierung mit new verdeckt, kann die verdeckende Methode niemals polymorph sein.
Vielleicht erahnen Sie an dieser Stelle schon, wozu virtuelle Methoden dienen. Erinnern wir uns: Eine Methode gilt als virtuell, wenn sie in der Basisklasse voll implementiert und mit dem Modifizierer virtual signiert ist, wie im folgenden Listing noch einmal gezeigt wird:
public class Luftfahrzeug {
public virtual void Starten() {
Console.WriteLine("Das Luftfahrzeug startet.");
}
}
Listing 4.18 Virtuelle Methode in der Basisklasse
Sie müssen eine virtuelle Methode als ein Angebot der Basisklasse an die ableitenden Klassen verstehen. Es ist das Angebot, die geerbte Methode entweder so zu erben, wie sie in der Basisklasse implementiert ist, sie bei Bedarf polymorph zu überschreiben oder eventuell auch einfach nur (nichtpolymorph) zu überdecken.
Polymorphes Überschreiben einer virtuellen Methode
Möchte die ableitende Klasse die geerbte Methode neu implementieren und soll die Methode polymorphes Verhalten zeigen, muss die überschreibende Methode mit dem Modifizierer override signiert werden, z. B.:
public class Flugzeug : Luftfahrzeug {
public override void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Listing 4.19 Polymorphes Überschreiben einer geerbten virtuellen Methode
Das Ergebnis des Aufrufs von Starten auf eine Basisklassenreferenz ist identisch mit dem Aufruf einer abstrakten Methode: Es wird die typspezifische Methode ausgeführt. An dieser Stelle lässt sich sofort schlussfolgern, dass der Modifizierer override grundsätzlich immer Polymorphie signalisiert.
Nicht-polymorphes Überdecken einer virtuellen Methode
Soll eine ableitende Klasse eine geerbte virtuelle Methode nichtpolymorph überschreiben, kommt der Modifizierer new ins Spiel:
public class Flugzeug : Luftfahrzeug {
public new void Starten() {
Console.WriteLine("Das Flugzeug startet.");
}
}
Listing 4.20 Nicht-polymorphes Überschreiben einer geerbten virtuellen Methode
Die mit new neu implementierte virtuelle Methode zeigt kein polymorphes Verhalten, wenn wir die Testanwendung starten. Auch hier können wir unter Berücksichtigung des Verdeckens klassisch implementierter Methoden sagen, dass im Zusammenhang mit dem Modifizierer new niemals polymorphes Verhalten eintritt.
Weiter gehende Betrachtungen
Es ist möglich, innerhalb einer Vererbungskette ein gemischtes Verhalten von Ausblendung und Überschreibung vorzusehen, wie das folgende Codefragment zeigt:
public class Luftfahrzeug {
public virtual void Starten() { }
}
public class Flugzeug : Luftfahrzeug {
public override void Starten () { [...] }
}
public class Segelflugzeug : Flugzeug {
public new void Starten() { [...] }
}
Listing 4.21 Überschreiben und Ausblenden in einer Vererbungskette
Luftfahrzeug bietet die virtuelle Methode Starten an, und die abgeleitete Klasse Flugzeug überschreibt diese mit override polymorph. Die nächste Ableitung in Segelflugzeug überdeckt jedoch nur noch mit new. Wenn Sie nun nach der Zuweisung
Luftfahrzeug lfzg = new Segelflugzeug();
auf der Referenz lfzg die Methode Starten aufrufen, wird die Methode Starten in Flugzeug ausgeführt, da diese die aus Luftfahrzeug geerbte Methode polymorph überschreibt. Starten zeigt aber in der Klasse Segelflugzeug wegen des Modifikators new kein polymorphes Verhalten mehr.
Das Überschreiben einer mit new überdeckenden Methode mit override ist hingegen nicht möglich, wie das folgende Codefragment zeigt:
public class Flugzeug : Luftfahrzeug {
public new void Starten() { [...] }
}
public class Segelflugzeug : Flugzeug {
public override void Starten () { [...] }
}
Listing 4.22 Fehlerhaftes Überschreiben und Ausblenden in einer Vererbungskette
Ein einmal verloren gegangenes polymorphes Verhalten kann nicht mehr reaktiviert werden.
Zusammenfassende Anmerkungen
Um polymorphes Verhalten einer Methode zu ermöglichen, muss sie in der Basisklasse als virtual definiert sein. Virtuelle Methoden haben immer einen Anweisungsblock und stellen ein Angebot an die ableitenden Klassen dar: Entweder wird die Methode einfach nur geerbt, oder sie wird in der ableitenden Klasse neu implementiert. Zur Umsetzung des zuletzt angeführten Falls gibt es wiederum zwei Möglichkeiten:
- Wird in der abgeleiteten Klasse die geerbte Methode mit dem Schlüsselwort override implementiert, wird die ursprüngliche Methode überschrieben – die abgeleitete Klasse akzeptiert das Angebot der Basisklasse. Ein Aufruf an eine Referenz der Basisklasse wird polymorph an den sich tatsächlich dahinter verbergenden Typ weitergeleitet.
- In der abgeleiteten Klasse wird eine virtuelle Methode mit dem Modifizierer new ausgeblendet. Dann verdeckt die Subklassenmethode die geerbte Implementierung der Basisklasse und zeigt kein polymorphes Verhalten.
Eine statische Methode kann nicht virtuell sein. Ebenso ist eine Kombination des Schlüsselworts virtual mit abstract oder override nicht zulässig. Hinter der Definition einer virtuellen Methode verbirgt sich die Absicht, polymorphes Verhalten zu ermöglichen. Daher ergibt es auch keinen Sinn, ein privates Klassenmitglied virtual zu deklarieren – es kommt zu einem Kompilierfehler. new und override schließen sich gegenseitig aus.
Entwickeln Sie eine ableitbare Klasse, sollten Sie grundsätzlich immer an die ableitenden Klassen denken. Polymorphie gehört zu den fundamentalen Prinzipien des objektorientierten Ansatzes. Methoden, die in abgeleiteten Klassen neu implementiert werden müssen, werden vermutlich immer polymorph überschrieben. Vergessen Sie daher die Angabe des Modifizierers virtual in keiner Methode – es sei denn, Sie haben handfeste Gründe, polymorphe Aufrufe bereits im Ansatz zu unterbinden.
Andererseits sollte man sich beim Einsatz von virtual auch darüber bewusst sein, dass die Laufzeitumgebung beim polymorphen Aufruf einer Methode dynamisch nach der typspezifischen Methode suchen muss, was natürlich zu Lasten der Performance geht. Man sollte folglich nicht prinzipiell virtual mit dem Gießkannenprinzip auf alle Methoden verteilen, sondern sich auch über den erwähnten Nachteil im Klaren sein.
Die Methode »ToString()« der Klasse »Object« überschreiben
Die Klasse Object ist die Basis aller .NET-Typen und vererbt jeder Klasse eine Reihe elementarer Methoden. Dazu gehört auch ToString. Diese Methode ist als virtuelle Methode definiert und ermöglicht daher polymorphes Überschreiben. ToString liefert per Vorgabe den kompletten Typbezeichner des aktuellen Objekts als Zeichenfolge an den Aufrufer zurück, wird aber von vielen Klassen des .NET Frameworks überschrieben. Aufgerufen auf einen int liefert ToString beispielsweise den von der int-Variablen beschriebenen Wert als Zeichenfolge.
Wir wollen das Angebot der Methode ToString wahrnehmen und sie in der Klasse Circle ebenfalls polymorph überschreiben. Der Aufruf der Methode soll dem Aufrufer typspezifische Angaben liefern.
public class Circle {
[...]
public override string ToString() {
return "Circle, R=" + Radius + ",Fläche=" + GetArea();
}
}
Listing 4.23 Überschreiben der geerbten Methode »Object.ToString()«
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.