4.4 Polymorphie 

In Abschnitt 4.2, »Der Problemfall geerbter Methoden«, haben Sie erfahren, dass die beiden Methoden Starten und Landen in der Klasse Luftfahrzeug unterschiedlich bereitgestellt werden können. Es ist nun an der Zeit, darauf einzugehen, welche weitergehenden Konsequenzen die drei verschiedenen Methodenimplementierungen in der Vererbungslinie 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[5];
arr[0] = new Flugzeug();
arr[1] = new Zeppelin();
arr[2] = new Hubschrauber();
arr[3] = new Hubschrauber();
arr[4] = new Flugzeug();
foreach(Luftfahrzeug temp in arr) {
temp.Starten();
}
Console.ReadLine();
}
Zuerst wird ein Array vom Typ Luftfahrzeug deklariert. Jedes Array-Element ist also vom Typ Luftfahrzeug. Weil die Klassen Flugzeug, Hubschrauber und Zeppelin von diesem Typ abgeleitet sind, kann jedem Array-Element auch die Referenz auf ein Objekt vom Typ der drei Subklassen zugewiesen werden:
arr[0] = new Flugzeug(); arr[1] = new Zeppelin(); arr[2] = 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 »Klassische« Methodenimplementierung 

Wir wollen an dieser Stelle zunächst die klassische Methodenimplementierung in der Basisklasse testen. Die drei Subklassen sollen die geerbte Methode Starten mit dem Modifizierer new überdecken. Stellvertretend sei an dieser Stelle nur der für uns wesentliche Ausschnitt aus der Klasse Flugzeug angegeben:
public class Luftfahrzeug { public void Starten() { Console.WriteLine("Das Luftfahrzeug startet."); } } public class Flugzeug : Luftfahrzeug { public new void Starten() { Console.WriteLine("Das Flugzeug startet."); } }
Starten wir die Anwendung, wird die Ausgabe im Konsolenfenster so lauten:
Das Luftfahrzeug startet. Das Luftfahrzeug startet. Das Luftfahrzeug startet. Das Luftfahrzeug startet. 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 klassenspezifische 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 Testcode 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."); } }
Ein anschließender Start der Anwendung bringt ein ganz anderes Ergebnis, als im ersten Versuch:
Das Flugzeug startet. Der Zeppelin startet. Der Hubschrauber startet. Der Hubschrauber startet. Das Flugzeug startet.
Tatsächlich werden nun die typspezifischen Methoden aufgerufen.
Anscheinend ist die Laufvariable temp 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 sie ist neben der Kapselung und der Vererbung die dritte Stütze 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 

Hinweis |
Den Programmcode zu dem folgenden Abschnitt finden Sie auf der Buch-DVD unter \Beispiele\Kapitel 4\Aircrafts. |
Ü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 Subklasse 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 virtuelle Methode, wenn sie in der Basisklasse voll implementiert und mit dem Modifizierer virtual signiert ist. Damit sieht die Klasse Luftfahrzeug wie folgt aus:
public class Luftfahrzeug { public virtual void Starten() { Console.WriteLine("Das Luftfahrzeug startet."); } public virtual void Landen() { Console.WriteLine("Das Luftfahrzeug landet."); } }
Sie müssen eine virtuelle Methode als ein Angebot der Basisklasse an die Subklassen 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 Subklasse 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."); } }
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.
Nichtpolymorphes Überdecken einer virtuellen Methode
Soll eine Subklasse eine geerbte virtuelle Methode nicht-polymorph überschreiben, kommt wiederum der Modifizierer new ins Spiel:
public class Flugzeug : Luftfahrzeug { public new void Starten() { Console.WriteLine("Das Flugzeug startet."); } }
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.
Weitergehende 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() { } }
Luftfahrzeug bietet die virtuelle Methode Starten an, und Flugzeug als Subklasse überschreibt diese mit override polymorph. Die nächste Ableitung in Segelflugzeug überdeckt Flugzeug 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.
Es ist hingegen nicht möglich, eine mit new überdeckende Methode durch override zu überschreiben, wie das folgende Codefragment zeigt:
// fehlerbehaftetes Überschreiben public class Flugzeug : Luftfahrzeug { public new void Starten() { } } public class Segelflugzeug : Flugzeug { public override void Starten () { } }
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 Subklassen 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 kann eine virtuelle Methode auch mit dem Modifizierer new ausgeblendet werden. Dann verdeckt die Methode in der Subklasse 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 macht es auch keinen Sinn, ein privates Klassenmitglied als virtual zu deklarieren – es kommt zu einem Kompilierfehler. new und override dürfen nicht für dasselbe Member verwendet werden, sie schließen sich gegenseitig aus.
Tipp |
Wenn Sie eine ableitbare Klasse entwickeln, sollten Sie grundsätzlich immer an die Subklassen denken. Polymorphie gehört zu den Fundamenten 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 dafür, polymorphe Aufrufe bereits im Ansatz zu unterbinden. |
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.
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();
}
}
4.4.4 Versiegelte Methoden 

Standardmäßig können alle Klassen abgeleitet werden. Ist dieses Verhalten für eine bestimmte Klasse nicht gewünscht, kann sie mit sealed versiegelt werden. Sie ist dann nicht weiter ableitbar. In ähnlicher Weise können Sie auch dem weiteren Überschreiben einer Methode einen Riegel vorschieben, indem Sie die Definition der Methode um den Modifizierer sealed ergänzen:
class Flugzeug : Luftfahrzeug { public sealed override void Starten() { Console.WriteLine("Das Flugzeug startet"); } }
Eine von Flugzeug abgeleitete Klasse erbt zwar die versiegelte Methode Starten, kann sie aber selbst nicht mit override überschreiben. Es ist jedoch möglich, in einer weiter abgeleiteten Klasse eine geerbte, versiegelte Methode mit new zu überdecken, um eine typspezifische Anpassung vornehmen zu können.
Der Modifizierer sealed kann nur zusammen mit override in einer Methodensignatur einer abgeleiteten Klasse verwendet werden, wenn die Methode in der Basisklasse als virtuelle Methode bereitgestellt wird. Die Kombination sealed new ist unzulässig, ebenso das alleinige Verwenden von sealed in der Methodensignatur.
4.4.5 Überladen einer Basisklassenmethode 

Oft ist es notwendig, die von einer Basisklasse geerbten Methoden in der Subklasse zu überladen, um ein Objekt vom Typ der Subklasse an speziellere Anforderungen anzupassen. Von einer Methodenüberladung wird bekanntlich dann gesprochen, wenn sich zwei gleichnamige Methoden einer Klasse nur durch ihre Parameterliste unterscheiden. Derselbe Begriff wird verwendet, wenn eine geerbte Methode in der Subklasse nach den Regeln der Methodenüberladung ergänzt werden muss.
Im folgenden Beispiel sehen Sie noch einmal die Klasse Flugzeug, die eine Methode namens Fliegen veröffentlicht:
public class Flugzeug { public virtual void Fliegen() { .. } }
Die Klasse Segelflugzeug beerbt Flugzeug, implementiert allerdings eine von der geerbten Methode Fliegen abweichende Parameterliste und überlädt diese:
public class Segelflugzeug : Flugzeug { public void Fliegen(double distance) { ... } }
Wird mit
Segelflugzeug glider = new Segelflugzeug();
ein Objekt vom Typ der abgeleiteten Klasse erzeugt, kann auf dessen Referenz mit zwei Methoden operiert werden, z. B. so:
glider.Fliegen(); glider.Fliegen(300);
4.4.6 Statische Member und Vererbung 

Statische Member werden an die abgeleiteten Klassen vererbt. Eine statische Methode kann man auf die Klasse anwenden, in der die Methode definiert ist, oder auf die Angabe der abgeleiteten Klasse. Bezogen auf das Projekt GeometricObjects, können Sie demnach die statische Methode Bigger entweder mit
Circle.Bigger(kreis1, kreis2);
oder mit
GraphicCircle.Bigger(kreis1, kreis2);
aufrufen. Dabei sind kreis1 und kreis2 Objekte vom Typ Circle.
Unzulässig ist die Definition einer statischen Methode mit virtual, override oder abstract. Wollen Sie dennoch eine geerbte statische Methode in der abgeleiteten Klasse neu implementieren, können Sie die geerbte Methode mit einer Neuimplementierung verdecken, die den Modifizierer new aufweist.