## 4.2 Der Problemfall geerbter Methoden Um das objektorientierte Konzept zu erläutern, habe ich mich bisher sehr häufig des Beispiels der beiden Klassen Circle und GraphicCircle bedient. Sie haben mit diesen beiden Klassen gelernt, wie die Struktur einer Klasse samt ihrer Felder, 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 drei Klassen Flugzeug, Hubschrauber und Zeppelin 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 sie ist daher in der Klasse Flugzeug implementiert. Ein Hubschrauber wiederum hat einen Rotordurchmesser, der Zeppelin ein Gasvolumen. Da alle drei abgeleiteten Typen sowohl starten als auch landen können, sind entsprechende Methoden in der Basisklasse Luftfahrzeug implementiert. Das nachfolgende Codefragment bildet die Situation ab. Um den Code kurz, einfach und überschaubar zu halten, wird die Datenkapselung nicht berücksichtigt und werden die Eigenschaften als öffentliche Felder beschrieben. Zudem enthalten die beiden Methoden nur einen symbolischen Code.
    public class Luftfahrzeug {
      public string Hersteller;
      public int Baujahr;
      public void Starten() {
        Console.WriteLine("Das Luftfahrzeug startet.");
      }
      public void Landen() {
        Console.WriteLine("Das Luftfahrzeug landet.");
      }
    }
    public class Flugzeug : Luftfahrzeug {
      public double Spannweite;
    }
    public class Hubschrauber : Luftfahrzeug {
      public double RotorDurchmesser;
    }
    public class Zeppelin : Luftfahrzeug {
      public double Gasvolumen;
    }
In Abbildung 4.4 sehen Sie die Zusammenhänge auf anschauliche Art. Hier klicken, um das Bild zu vergrößern 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 beiden Methoden Starten und Landen in der Basisklasse Luftfahrzeug richtig? Ein Flugzeug wird anders starten als ein Hubschrauber und ein Hubschrauber wiederum anders als ein Zeppelin. Ein Flugzeug benötigt eine Startbahn mit einer Mindestlänge, um überhaupt abheben zu können, während für einen Hubschrauber bereits eine freie Fläche in einer Größe genügt, die gewährleistet, dass die Rotorspitzen keine Hindernisse streifen. Um einen Zeppelin zu starten, wird eine Mannschaft benötigt, die das Halteseil löst und das Luftschiff zum Starten freigibt. Ähnliche Überlegungen können auch mit der Methode Landen angestellt werden. Ein neues Problem offenbart sich plötzlich: Wie kann das Start- und Landeverhalten in der Basisklasse implementiert werden, wenn es für die Objekte der abgeleiteten Typen Zeppelin, Hubschrauber und Flugzeug unterschiedlich beschrieben werden muss? Mit der Lösung dieser Problematik, dass gleichnamige Methoden in verschiedenen Klassen unterschiedliche Implementierungen haben müssen, werden wir uns im Folgenden beschäftigen. Prinzipiell bieten sich drei Lösungsansätze an: Wir verdecken die geerbten Methoden der Basisklasse in der Subklasse 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 beiden Methoden Starten und Landen wie folgt codiert sind:
    public class Luftfahrzeug {
      public void Starten() {
        Console.WriteLine("Das Luftfahrzeug startet.");
      }
      public void Landen() {
        Console.WriteLine("Das Luftfahrzeug landet.");
      }
    }
Unsere drei abgeleiteten Klassen haben sehr wohl ein Interesse an den Methoden Starten und Landen – allerdings mit einer typspezifischen Implementierung. In der abgeleiteten Klasse müssen daher die geerbten Methoden der Basisklasse ausgeblendet (bzw. verdeckt) und neu codiert werden. Dabei hilft uns der Modifizierer new weiter. Exemplarisch sei das an der Klasse Flugzeug gezeigt, es gilt aber natürlich in gleicher Weise auch für Zeppelin und Hubschrauber:
    public class Flugzeug : Luftfahrzeug {
      public new void Starten() {
        Console.WriteLine("Das Flugzeug startet.");
      }
      public new void Landen() {
        Console.WriteLine("Das Flugzeug landet.");
      }
    }
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 dann 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, dass Sie die Methodendefinition in der Subklasse um den Modifizierer new ergänzen. Wird eine Basisklassenmethode in der abgeleiteten Klasse verdeckt, wird beim Aufruf der Methode auf Objekten vom Typ der Subklasse immer die verdeckende Version ausgeführt. Die Anweisungen
    Flugzeug flg = new Flugzeug();
    flg.Starten();
führen im Befehlsfenster zu der Ausgabe: Das Flugzeug startet.
Hinweis
In gleicher Weise, wie eine geerbte Instanzmethode in einer ableitenden Klasse verdeckt werden kann, lassen sich mit new auch Eigenschaftsmethoden, Felder und statische Komponenten einer Basisklasse verdecken und durch eine typspezifische Implementierung ersetzen.
#### Die Sichtbarkeit eines verdeckenden Klassenmitglieds Zugriffsmodifizierer beschreiben die Sichtbarkeit. Ein als public deklariertes Mitglied ist über die Grenzen der aktuellen Anwendung hinaus bekannt, während der Modifizierer internal die Sichtbarkeit auf die aktuelle Assemblierung beschränkt. private Klassenmitglieder hingegen sind nur in der definierenden Klasse sichtbar. Ein verdeckendes Member muss nicht zwangsläufig denselben Zugriffsmodifizierer haben wie das in der Basisklasse. Machen wir uns das kurz an der Klasse Flugzeug klar, und verdecken wir die geerbte Methode Starten der Klasse Luftfahrzeug durch eine private-Implementierung in Flugzeug.
    public class Flugzeug : Luftfahrzeug {
      ...
      private new void Starten() {
        Console.WriteLine("Das Flugzeug startet.");
      }
    }
Die verdeckende Methode Starten ist nun nur innerhalb von Flugzeug sichtbar. Einen interessanten Effekt stellen wir fest, wenn wir jetzt den folgenden Code schreiben:
    static void Main(string[] args) {
      Flugzeug flg = new Flugzeug();
      flg.Starten();
    }
Im Konsolenfenster wird Das Luftfahrzeug startet. ausgegeben. Aus allem, was bisher gesagt worden ist, müssen wir die Schlussfolgerung ziehen, dass das vollständige Ausblenden eines geerbten Mitglieds nicht möglich ist, auch nicht durch »Privatisierung«. Das führt uns zu folgender Erkenntnis:
Merksatz
Grundsätzlich werden alle Member der Basisklasse geerbt. Davon gibt es keine Ausnahme. Auch das Ausblenden durch Privatisierung in der erbenden Klasse ist nicht möglich.
Wollen Sie unter keinen Umständen eine Methode aus der Basisklasse erben, bleibt Ihnen nur ein Weg: Sie müssen das Konzept Ihrer Klassenhierarchie neu überdenken. ### 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 geerbten Methoden Starten und Landen überdecken und 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 Klassen, die von der Klasse Luftfahrzeug abgeleitet werden, dazu zwingen, die Methoden Starten und Landen neu zu implementieren? Gehen wir noch einen Schritt weiter, und stellen wir uns die Frage, ob wir überhaupt dann noch Code in den Methoden Starten und Landen der Klasse Luftfahrzeug benötigen. Anscheinend nicht. Dass wir die beiden Methoden in der Basisklasse definiert haben, liegt im Grunde genommen nur daran, dass wir diese Methoden in den ableitenden Klassen bereitstellen wollen. Mit dieser Erkenntnis mag die Lösung der aufgezeigten Problematik im ersten Moment verblüffen: Tatsächlich werden Starten und Landen in der Basisklasse nicht implementiert – sie bleiben einfach ohne Programmcode. Damit wäre aber noch nicht sichergestellt, dass die Subklassen 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 aufweisen, als abstrakte Methoden bezeichnet. Neben den Methoden, die das Verhalten eines Typs beschreiben, können auch Eigenschaften als abstrakt definiert werden. Abstrakte Methoden werden durch die Angabe des abstract-Modifizierers in der Methodensignatur gekennzeichnet, am Beispiel unserer Methoden Starten und Landen also durch:
    public abstract void Starten();
    public abstract void Landen();
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 Subklassen als Methodengeber 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 ein durch abstract gekennzeichnetes Member enthält, ist ihrerseits selbst abstrakt und wird deshalb als abstrakte Klasse bezeichnet. Abstrakte Klassen machen nur dann Sinn, wenn von ihnen weitere Klassen 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();
      public abstract void Starten();
      ...
    }
Neben abstrakten Methoden darf eine abstrakte Klasse auch vollständig implementierte Methoden und Eigenschaften bereitstellen. So könnte die Klasse Luftfahrzeug beispielsweise eine Methode Fliegen enthalten, wie das folgende Codefragment zeigt:
    public abstract class Luftfahrzeug {
      private string hersteller;
      // abstrakte Methoden
      public abstract void Starten();
      public abstract void Landen();
      // konkrete Methode
      public void Fliegen() {
        ...
      }
    }
Die Signatur einer Methode und infolgedessen auch der dazugehörigen Klasse mit dem Modifizierer abstract kommt einer Forderung gleich: Alle nicht abstrakten 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 in jedem Fall ebenfalls als 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.
Hinweis
Eine Klasse, die eine als 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, ein abstraktes Mitglied zu enthalten. Eine Klasse kann auch dann abstrakt sein, wenn keines ihrer Member abstrakt ist. Auf diese Weise wird eine Klasse nicht instanziierbar, und das Ableiten von dieser Klasse wird 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 werden die abstrakten Methoden Starten und Landen der Basisklasse überschrieben. Um zu kennzeichnen, dass eine abstrakte Basisklassenmethode überschrieben wird, verwenden Sie den Modifizierer override:
    class Hubschrauber : Luftfahrzeug {
      public override void Starten() {
        Console.WriteLine("Der Hubschrauber startet.");
      }
      public override void Landen() {
        Console.WriteLine("Der Hubschrauber landet.");
      }
    }
Sollten Sie dieses Beispiel ausprobieren, müssen Sie Starten und Landen selbstverständlich auch in den Klassen Flugzeug und Zeppelin mit override überschreiben. ### 4.2.3 Virtuelle Methoden Widmen wir uns nun der dritten anfangs aufgezeigten Variante, den virtuellen Methoden. Unser Ausgangspunkt sei dabei folgender: Wir wollen Starten und Landen wieder in der Basisklasse vollständig implementieren. Damit wären wir wieder am Ausgangspunkt angelangt – mit einem kleinen Unterschied: Wir ergänzen die Methoden Starten und Landen mit dem Modifizierer virtual. Dann 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.");
      }
    }
Nun sind die beiden Methoden als virtuelle Methoden in der Basisklasse definiert. Eine Subklasse hat nun die Wahl zwischen drei Alternativen: Die Subklasse erbt die Methoden, ohne eine eigene, typspezifische Implementierung vorzusehen, also:
    public class Flugzeug : Luftfahrzeug { }
Die Subklasse verdeckt die geerbten Methoden mit new, hier also:
    public class Flugzeug : Luftfahrzeug {
      public new void Starten() {
        Console.WriteLine("Das Flugzeug startet.");
      }
      public new void Landen() {
        Console.WriteLine("Das Flugzeug landet.");
      }
    }
Die Subklasse überschreibt die geerbten Methoden mit override, also:
    public class Flugzeug : Luftfahrzeug {
      public override void Starten() {
        Console.WriteLine("Das Flugzeug startet.");
      }
      public override void Landen() {
        Console.WriteLine("Das Flugzeug landet.");
      }
    }