1. Einführung
Wenn ein Anfänger das Wort Vererbung hört, denkt er vielleicht: easy, einfach eine bestehende Klasse nehmen, ein bisschen erweitern oder ändern – fertig! In Wirklichkeit gibt's aber viele Details. Fehler beim Vererben können sich unterschiedlich zeigen: falsche Signaturen, vergessene Schlüsselwörter, schlechtes Hierarchiedesign – das alles führt zu fiesen Bugs.
Manchmal tauchen Fehler sofort auf: der Code kompiliert nicht. In anderen Fällen machen sich die Bugs erst zur Laufzeit bemerkbar, wenn dein neuer SuperMegaLogger ganz was anderes schreibt als erwartet – oder gar nichts. Frust, Enttäuschung, stundenlanges Debuggen – kennst du das?
Lass uns gemeinsam die häufigsten Fehler beim Vererben und Überschreiben anschauen und unseren Code auf dem Weg "gesund machen".
2. Vergessenes virtual: Warum kann ich die Methode nicht überschreiben?
Das Problem
In C# kannst du eine Methode nur dann mit override überschreiben, wenn sie in der Basisklasse mit virtual, abstract oder selbst als override (in der Vererbungskette) deklariert ist. Fehlt das, bekommst du beim Versuch, in der abgeleiteten Klasse override zu schreiben, einen Compilerfehler.
class Animal
{
public void Speak()
{
Console.WriteLine("Das Tier sagt etwas.");
}
}
class Cat : Animal
{
// Compilerfehler! Die Methode in der Basisklasse ist weder virtual, abstract noch override.
public override void Speak()
{
Console.WriteLine("Miau!");
}
}
Der Fehler sieht ungefähr so aus: "'Cat.Speak()': kann das geerbte Element 'Animal.Speak()' nicht überschreiben, weil es nicht als virtual, abstract oder override markiert ist".
Wie tritt man nicht in diese Falle?
Wenn du Methoden überschreiben willst, deklariere sie in der Basisklasse immer als virtual. Wenn du Klassen für Erweiterungen designst, überleg dir immer, welche Methoden sinnvoll zum Überschreiben sind.
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Das Tier macht ein Geräusch.");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Miau!");
}
}
Warum das Ganze?
Mit virtual erlaubst du explizit, dass Nachfolger das Verhalten ändern dürfen, und der Compiler hilft dir – er lässt dich nicht versehentlich die "falsche" Methode überschreiben.
3. Fehler mit override und new
Manchmal gibt's das umgekehrte Problem: Der Autor der abgeleiteten Klasse will eine Methode "überschreiben", aber in der Basisklasse ist sie nicht virtual. Der Compiler lässt dann kein override zu, aber new geht. Das ist aber ein ganz anderes Verhalten!
class Dog : Animal
{
// Das ist kein override, sondern versteckt (hiding) die Methode der Basisklasse.
public new void Speak()
{
Console.WriteLine("Wuff!");
}
}
Rufst du die Methode über eine Referenz vom Typ Dog auf – alles gut:
Dog dog = new Dog();
dog.Speak(); // "Wuff!"
Aber wenn du das Objekt über eine Referenz vom Basistyp ansprichst, wird die Basismethode aufgerufen:
Animal dog2 = new Dog();
dog2.Speak(); // "Das Tier macht ein Geräusch."
Erklärung:
Das Schlüsselwort new überschreibt die Methode NICHT, sondern versteckt sie nur. Das nennt man "hiding". Das kann verwirren, wenn das polymorphe Verhalten nicht wie erwartet funktioniert.
Wie vermeidest du Fallen mit new vs override?
Willst du klassisches Überschreiben mit Polymorphismus – nutze virtual/override. Wenn du wirklich neue Funktionalität hinzufügst (oder bewusst das Verhalten der Basismethode verstecken willst – Vorsicht!), dann new.
| Situation | Schlüsselwort | Polymorphismus? | Verhalten bei Zugriff über Basistyp |
|---|---|---|---|
| Verhalten ändern | override | Ja | Methode der abgeleiteten Klasse wird aufgerufen |
| Methode verstecken/ersetzen | new | Nein | Methode der Basisklasse wird aufgerufen |
4. Nicht übereinstimmende Methodensignaturen
Anfänger (und manchmal auch Profis) machen Fehler, wenn sie die Parametertypen oder den Rückgabetyp in der abgeleiteten Methode ändern. Wenn die Basismethode z.B. public virtual void Print(string message) ist, und in der abgeleiteten Klasse steht public override void Print(object message), ist das KEIN override, sondern eine neue Methode.
class Printer
{
public virtual void Print(string msg)
{
Console.WriteLine("Basisdrucker: " + msg);
}
}
class SmartPrinter : Printer
{
// Compilerfehler! Signatur stimmt nicht mit der Basismethode überein.
public override void Print(object msg)
{
Console.WriteLine("Smart-Drucker: " + msg);
}
}
Tipp:
Name, Rückgabetyp und Parameter (Typen, Anzahl, Reihenfolge) müssen übereinstimmen.
Wenn du versehentlich einen Parametertyp änderst oder dich im Namen vertippst – der Compiler warnt dich.
5. Falsche Zugriffsmodifizierer
Noch ein Stolperstein: Zugriffsmodifizierer. Die Methode in der abgeleiteten Klasse darf nicht restriktiver sein als in der Basisklasse. Beispiel für schlechten Code:
public class Vehicle
{
public virtual void StartEngine() { /* ... */ }
}
public class Car : Vehicle
{
// Fehler! 'private' ist restriktiver als 'public' in der Basismethode.
private override void StartEngine() { /* ... */ }
}
Was tun?
Der Zugriffsmodifizierer in der abgeleiteten Methode muss gleich oder offener sein als in der Basisklasse. Meistens also public oder protected.
6. Vergessene/überflüssige abstrakte Methode
Wenn in der Basisklasse eine Methode als abstract deklariert ist, MUSS sie in der abgeleiteten Klasse mit override implementiert werden, sonst wird die Klasse auch abstrakt (und kann nicht instanziiert werden).
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
// Fehler! Abstrakte Methode Area() nicht implementiert
}
Lösung:
Du musst die Methode implementieren:
class Circle : Shape
{
public override double Area()
{
return 3.14 * 2 * 2; // Ungefähr...
}
}
7. Aufruf der Basismethode: base.Method()
Manchmal willst du die Basismethode nicht komplett ersetzen, sondern etwas hinzufügen. Viele vergessen (oder wissen nicht), dass du in der abgeleiteten Methode die Basismethode mit base aufrufen kannst.
class Logger
{
public virtual void Log(string msg)
{
Console.WriteLine("Basis-Log: " + msg);
}
}
class FancyLogger : Logger
{
public override void Log(string msg)
{
// Du kannst "Features" hinzufügen und die Basismethode aufrufen:
Console.WriteLine("[FANCY] " + msg);
base.Log(msg);
}
}
Erklärung:
Wenn du base.Log(msg) nicht aufrufst, geht die Logik der Basisklasse komplett verloren.
8. Aufruf des Basiskonstruktors (base)
Wenn die Basisklasse zwingende Parameter im Konstruktor verlangt, muss die abgeleitete Klasse explizit den passenden Konstruktor mit base aufrufen.
class Engine
{
public Engine(int cylinders)
{
Console.WriteLine("Engine mit Zylindern: " + cylinders);
}
}
class RaceEngine : Engine
{
// Compilerfehler! Engine hat keinen parameterlosen Konstruktor.
public RaceEngine() { }
}
// Korrigierte Variante:
class RaceEngine2 : Engine
{
public RaceEngine2() : base(8) // Ruft explizit den Basiskonstruktor auf
{
Console.WriteLine("Rennmotor ist bereit!");
}
}
9. "Vergessenes" sealed bei Methoden
Manchmal willst du verhindern, dass eine Methode in weiteren abgeleiteten Klassen überschrieben wird – dafür gibt's in C# das Schlüsselwort sealed zusammen mit override. Wenn du das nicht nutzt, kann jemand deine Methode überschreiben und damit vielleicht deine Logik kaputt machen.
class Hero
{
public virtual void Attack() => Console.WriteLine("Held greift an!");
}
class Warrior : Hero
{
public sealed override void Attack() => Console.WriteLine("Krieger schlägt zu!");
}
class Mutant : Warrior
{
// Fehler! Methode Attack ist oben als sealed markiert.
// public override void Attack() { ... }
}
Erklärung:
So "versiegelst" du die Implementierung auf dieser Ebene und Unterklassen können sie nicht mehr ändern.
10. Überflüssige oder falsche Überladungen statt Überschreibungen
Manche verwechseln Überladen (overloading) mit Überschreiben (overriding). Überladen heißt: gleiche Methode, aber andere Signatur (z.B. andere Parameteranzahl), hat aber nichts mit Polymorphismus zu tun.
class Animal
{
public virtual void Eat()
{
Console.WriteLine("Das Tier frisst.");
}
}
class Panda : Animal
{
// Das ist KEIN override! Nur eine neue Überladung.
public void Eat(string what)
{
Console.WriteLine("Panda frisst: " + what);
}
}
...
Animal a = new Panda();
a.Eat(); // Wenn die Methode überschrieben ist – wird die Implementierung der abgeleiteten Klasse aufgerufen. Wenn nicht – die der Basisklasse
// a.Eat("Bambus"); // Compilerfehler: Methode gibt's bei Animal nicht
Für polymorphes Verhalten musst du wirklich überschreiben, nicht nur überladen.
11. Formale und informelle Designfehler
Wenn Klassen schlecht designt sind, werden die Vererbungslisten schnell unübersichtlich:
- interne "Karussells" von overrides ohne sinnvollen base.-Aufruf,
- inkonsistente Modifizierer,
- zu tiefe oder unklare Hierarchien,
- Methoden, deren Logik über mehrere Vererbungsebenen "verschmiert" ist,
- fehlende Kommentare bei virtual/abstract-Methoden,
- unerwartete Nebeneffekte (z.B. Aufruf einer virtuellen Methode im Basisklassen-Konstruktor, wenn die abgeleitete Klasse noch nicht fertig initialisiert ist).
Tipp:
Wenn du den Überblick verlierst – nimm dir ein Blatt Papier (oldschool, aber effektiv!) und zeichne die Hierarchie auf, schreib dazu, welche Methode wo ist, wer sie überschreibt, was aufruft und wo base. stehen muss.
In echten Projekten ist es auch extrem wichtig, in der Doku zu vermerken, was beim Überschreiben welcher Methode erwartet wird.
GO TO FULL VERSION