Menu

C# Vererbung: base, virtual, override, sealed und Polymorphie

Wie Klassenvererbung in C# funktioniert: mit einem Doppelpunkt ableiten, was vererbt wird und was nicht, die Basisklasse mit base aufrufen, virtual und override, Verdecken mit new (und die Ausgabe, die jeden überrascht), sealed, Polymorphie und Casts nach oben und unten in einer Hierarchie.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Mit Vererbung baut eine Klasse auf einer anderen auf. Die abgeleitete Klasse bekommt die Member der Basisklasse, fügt eigene hinzu und darf das Verhalten ersetzen, das die Basisklasse zum Ersetzen freigibt. Zusammen mit virtual-Methoden ergibt das Polymorphie: Code, der gegen die Basisklasse geschrieben ist, führt das richtige abgeleitete Verhalten aus, ohne zu wissen, welche abgeleitete Klasse er vor sich hat.

Eine Klasse ableiten

Setze die Basisklasse nach einen Doppelpunkt. Die abgeleitete Klasse hat alles, was die Basisklasse hat, plus das, was sie selbst deklariert:

Ausgabe:

TX-19: 20 km, 2 fares
True

Was eine abgeleitete Klasse bekommt und was nicht:

  • Vererbt: Felder, Properties, Methoden, Events und verschachtelte Typen. Alles ist physisch im Objekt vorhanden.
  • Zugänglich: nur das, was die Basis erlaubt: Member mit public, protected und internal. Ein privater Member von Vehicle existiert in jedem Taxi, aber der Code von Taxi kann ihn nicht ansprechen. Deshalb hat Km ein private set: Taxi kann den Wert lesen und ändert ihn nur über Drive.
  • Nicht vererbt: Konstruktoren. Taxi muss eigene deklarieren und mit : base(plate) an einen von Vehicle verketten. Die Seite zu Konstruktoren zeigt, in welcher Reihenfolge beide laufen.

Jede Klasse leitet letztlich von object ab, deshalb hat jedes Objekt ToString(), Equals() und GetHashCode().

virtual und override

Eine Basisklasse markiert eine Methode als virtual, um zu sagen „abgeleitete Klassen dürfen eine eigene Version liefern“. Eine abgeleitete Klasse ersetzt sie mit override. Innerhalb der Überschreibung ruft base.Method() die Version der Basisklasse auf.

Ausgabe:

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

Die Schleifenvariable hat den Typ Notification, und trotzdem stellt sich jedes Objekt auf seine eigene Weise dar. Das ist Polymorphie: Der Aufruf n.Render(...) wird zur Laufzeit anhand des tatsächlichen Typs des Objekts aufgelöst. Beachte auch, dass base.Render in Email Channel verwendet, das selbst virtuell ist, die Basismethode gibt also email aus, nicht generic. Ein virtueller Aufruf innerhalb der Basisklasse erreicht trotzdem die Überschreibung.

Auch Properties können virtuell sein, wie Channel zeigt. Felder nicht.

Der Compiler besteht auf beiden Schlüsselwörtern. override an einer Methode, die nicht virtual ist, ist Fehler CS0506 („cannot override inherited member ... because it is not marked virtual, abstract, or override“). override wegzulassen, wenn die Basismethode virtuell ist, ist nur eine Warnung, ändert aber die Bedeutung vollständig, wie der nächste Abschnitt zeigt.

new gegenüber override: verdecken statt überschreiben

Deklariert eine abgeleitete Klasse eine Methode mit derselben Signatur wie eine Basismethode, ohne override zu schreiben, verdeckt sie die Basismethode. Der Compiler warnt (CS0114 bei einer virtuellen Basismethode, sonst CS0108) und schlägt das Schlüsselwort new vor, das die Warnung zum Schweigen bringt, das Verdecken aber beibehält:

Ausgabe:

Sales report
Report
Draft report

b und c sind dieselbe Art Objekt und geben trotzdem verschiedene Titel aus. Mit new hängt die gewählte Methode vom Typ der Variable ab, entschieden zur Kompilierzeit. Code, der Berichte als Report behandelt (eine Liste, ein Methodenparameter, ein Framework-Callback), sieht DraftReport.Title nie. Das ist fast nie gewollt. Nimm override für Polymorphie; new existiert vor allem für den Fall, dass eine Basisklasse, die du nicht kontrollierst, einen Member hinzufügt, dessen Name mit einem kollidiert, den du schon hast.

sealed

sealed an einer Klasse verbietet es, von ihr abzuleiten:

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string ist sealed, ebenso viele Framework-Typen. An einer Überschreibung beendet sealed die Kette auf dieser Ebene:

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

Eine Klasse für Vererbung zu entwerfen ist Arbeit: entscheiden, was virtuell ist, worauf sich abgeleitete Klassen verlassen dürfen, in welcher Reihenfolge Dinge passieren. Eine Klasse, die nicht so entworfen wurde, ist versiegelt sicherer, und das Versiegeln lässt sich später rückgängig machen, ohne jemanden zu brechen, während sich das Entsiegeln nicht zurücknehmen lässt, sobald andere von dir ableiten. Aufrufe von Membern versiegelter Klassen können außerdem etwas schneller sein, weil die Runtime weiß, dass es keine Überschreibung gibt.

Eine Basisklasse, viele Interfaces

Eine C#-Klasse hat genau eine Basisklasse. class Admin : User, Employee ist Fehler CS1721 („cannot have multiple base classes“). Eine Klasse kann aber neben ihrer Basisklasse beliebig viele Interfaces implementieren:

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

Nimm eine Basisklasse für „ist eine Art von und teilt die Implementierung mit“ und Interfaces für „kann“. Wenn die Basisklasse nur existiert, um abgeleitete Klassen zu zwingen, bestimmte Methoden auszufüllen, ist eine abstrakte Klasse das Werkzeug.

Casts nach oben und unten in der Hierarchie

Ein abgeleitetes Objekt lässt sich immer dort verwenden, wo sein Basistyp erwartet wird. Dieser Upcast ist implizit und kann nicht scheitern. Die andere Richtung, ein Downcast, braucht einen expliziten Cast und scheitert zur Laufzeit, wenn das Objekt nicht diesen Typ hat:

Ausgabe:

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

Dog d = pet; ohne den Cast zu schreiben ist ein Kompilierfehler (CS0266: „An explicit conversion exists (are you missing a cast?)“), weil der Compiler nur weiß, dass pet irgendein Animal ist. Nimm lieber is mit einer Variable, wenn das Objekt einen anderen Typ haben könnte, und einen einfachen Cast nur, wenn alles andere ein Bug wäre. Häufiges Downcasting ist ein Warnsignal im Design: Meist gehört das Verhalten in eine virtuelle Methode der Basisklasse. Die Seite zu Pattern Matching behandelt die Formen von is vollständig.

Vererbung und Collections

Polymorphie funktioniert Element für Element, aber generische Collections eines abgeleiteten Typs sind keine Collections des Basistyps: List<Animal> animals = new List<Dog>(); kompiliert nicht, weil die Liste dann eine Cat annehmen würde. Schreibgeschützte Sichten sind kovariant, IEnumerable<Animal> animals = new List<Dog>(); ist also in Ordnung. Die Seite zu Generics erklärt, warum.

Häufige Fehler

  • override vergessen. Die Methode kompiliert mit einer Warnung und verdeckt stillschweigend, statt zu überschreiben. Behandle CS0114 als Fehler.
  • Alles virtual machen. Jeder virtuelle Member ist ein Versprechen an abgeleitete Klassen darüber, wann er aufgerufen wird und was er annehmen darf. Markiere nur die Erweiterungspunkte, die du beabsichtigst.
  • Tiefe Hierarchien. Drei oder vier Vererbungsebenen machen es schwer zu wissen, welche Version einer Methode läuft. Komposition, also eine Klasse, die ein anderes Objekt hält und aufruft, ist oft einfacher als eine weitere Ebene.
  • Virtuelle Methoden aus einem Konstruktor aufrufen. Die Überschreibung läuft vor dem Rumpf des abgeleiteten Konstruktors, jedes Feld, das dieser Rumpf zuweist, hat also noch seinen Standardwert.
  • Vererbung nur zur Wiederverwendung von Code. Erbt ein Stack von List, können Aufrufer mit Insert mitten in deinen Stack einfügen. Halte stattdessen eine List in einem privaten Feld.

Häufig gestellte Fragen

Wie funktioniert Vererbung in C#?

Eine Klasse nennt nach einem Doppelpunkt eine Basisklasse: class Dog : Animal. Die abgeleitete Klasse bekommt alle Member der Basisklasse (Felder, Properties, Methoden, Events), kann eigene hinzufügen und die überschreiben, die die Basis als virtual markiert hat. Konstruktoren werden nicht vererbt, und private-Member sind zwar im Objekt vorhanden, aber aus der abgeleiteten Klasse nicht zugänglich.

Was ist der Unterschied zwischen virtual und override in C#?

virtual steht an der Methode der Basisklasse und sagt „abgeleitete Klassen dürfen das ersetzen“. override steht an der Methode der abgeleiteten Klasse und ersetzt sie. Beide sind nötig: Eine Methode zu überschreiben, die nicht virtual, abstract oder bereits override ist, ist Fehler CS0506. Beim Aufruf einer virtuellen Methode führt die Runtime die Version für den tatsächlichen Typ des Objekts aus, nicht für den Typ der Variable.

Was ist der Unterschied zwischen new und override in C#?

override ersetzt die Basismethode für jeden Aufrufer, auch für Code, der das Objekt über eine Variable der Basisklasse hält. new verdeckt sie nur: Code, der das Objekt als abgeleiteten Typ sieht, ruft die neue Methode auf, während Code, der es als Basistyp sieht, weiterhin die Basismethode aufruft. Base b = new Derived(); b.M(); führt also mit override Derived.M aus und mit new Base.M.

Unterstützt C# Mehrfachvererbung?

Nicht für Klassen: Eine Klasse hat genau eine Basisklasse, und zwei aufzulisten ist Fehler CS1721. Eine Klasse kann aber beliebig viele Interfaces implementieren, und so bildet C# ab, dass „dieser Typ mehrere Dinge kann“. Seit C# 8 können Interfaces außerdem Standardimplementierungen von Methoden enthalten.

Was bedeutet sealed in C#?

Eine sealed-Klasse kann nicht als Basisklasse verwendet werden; von ihr abzuleiten ist Fehler CS0509. string ist zum Beispiel sealed. An einer Methode verhindert sealed override, dass Klassen weiter unten in der Hierarchie sie erneut überschreiben (CS0239). Klassen zu versiegeln, die nicht für Vererbung entworfen wurden, ist ein vernünftiger Standard.

Wie rufe ich in C# die Methode der Basisklasse auf?

Mit base.MethodName(...) innerhalb der abgeleiteten Klasse, meist innerhalb der Überschreibung: public override string Describe() => base.Describe() + " with GPS";. Bei Konstruktoren wählst du mit : base(...) nach der Parameterliste, welcher Basiskonstruktor läuft.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S