1. Einführung
Stell dir eine Welt ohne Hierarchien vor: Tausende Klassen Person, Animal, Vehicle – und alles komplett getrennt. Kein Wunder, dass Programmierer in so einer Welt nicht mal bis zum Mittagessen durchhalten würden – völliges Chaos! In echten Projekten brauchen wir oft Objekte, die etwas gemeinsam können (zum Beispiel: alle Tiere können sich bewegen), aber trotzdem ihre eigenen Besonderheiten haben (Fische schwimmen, Vögel fliegen).
Genau Klassenhierarchien drücken die Beziehungen zwischen Entitäten aus, damit Programmieren kreativ bleibt und nicht zum endlosen Copy-Paste-Kampf wird.
Schauen wir uns ein Beispiel an. Angenommen, wir haben eine Basisklasse Animal. Alle Tiere können Geräusche machen. Aber nur Katzen miauen, Hunde bellen und Papageien können sogar ein paar Witze erzählen. Das wollen wir im Code hierarchisch ausdrücken.
Basisklasse
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Basismethode: kann in Nachkommen überschrieben werden
public virtual void Speak()
{
Console.WriteLine("Tier macht irgendein Geräusch...");
}
}
Hier haben wir das Wort virtual zur Methode Speak() hinzugefügt. Das ist wie ein Hinweis: "Hey, Kindklassen, wenn ihr wollt – überschreibt diese Methode ruhig".
Wir bauen die Hierarchie: abgeleitete Klassen
Jetzt nehmen wir eine Klasse Cat, die von Animal erbt:
public class Cat : Animal
{
public Cat(string name) : base(name) { }
// Überschreiben von Speak – Katzen können ja nicht einfach brüllen!
public override void Speak()
{
Console.WriteLine($"{Name} sagt: Miau!");
}
}
Und die Klasse Dog:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} sagt: Wuff!");
}
}
Und wenn wir ein ganz normales Tier haben, das nicht sprechen kann? Dann kann man einfach die Basisklasse nutzen, ohne etwas zu überschreiben.
Visualisierung – Hierarchiebaum
. Animal
/ \
Cat Dog
- Animal – Basisklasse
- Cat, Dog – Kindklassen (abgeleitete Klassen)
2. Lass uns Code schreiben, der so eine Hierarchie nutzt
Wir machen weiter mit unserer Konsolenanwendung.
Angenommen, wir haben eine Sammlung von Tieren und wollen, dass jedes etwas Typisches sagt:
Animal[] zoo = new Animal[]
{
new Cat("Barsik"),
new Dog("Rex"),
new Animal("Geheimnisvolles Wesen")
};
foreach (Animal animal in zoo)
{
animal.Speak();
}
Erwartete Ausgabe:
Barsik sagt: Miau!
Rex sagt: Wuff!
Tier macht irgendein Geräusch...
So wird dank Hierarchie und Polymorphismus (wir schauen uns das gleich noch genauer an, aber im Kern: die richtige Version der Methode wird je nach tatsächlichem Objekttyp aufgerufen) deine App flexibel und erweiterbar.
3. Neue Methoden und Felder hinzufügen
Okay, "Vertonung" läuft. Aber alle Tiere sind zu langweilig. Zum Beispiel kann eine Katze neun Leben haben, und ein Hund kann Stöckchen holen.
Einzigartiges Verhalten hinzufügen
In der Kindklasse kann man eigene Methoden und Felder hinzufügen:
public class Cat : Animal
{
public int Lives { get; private set; } = 9;
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} sagt: Miau! Ich habe {Lives} Leben.");
}
public void LoseLife()
{
if (Lives > 0)
{
Lives--;
Console.WriteLine($"{Name} hat ein Leben verloren. Übrig: {Lives}");
}
else
{
Console.WriteLine($"{Name} hat schon alle Leben verbraucht!");
}
}
}
Im Code anwenden:
var barsik = new Cat("Barsik");
barsik.Speak(); // Barsik sagt: Miau! Ich habe 9 Leben.
barsik.LoseLife(); // Barsik hat ein Leben verloren. Übrig: 8
Neue Klassen hinzufügen: "Zoo" erweitern
Du kannst jetzt abgeleitete Klassen erstellen. Fügen wir zum Beispiel einen Papagei hinzu:
public class Parrot : Animal
{
public Parrot(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"{Name} sagt: Hallo, Mensch!");
}
public void Repeat(string phrase)
{
Console.WriteLine($"{Name} wiederholt: {phrase}");
}
}
Jetzt kannst du das System easy erweitern, ohne alten Code anzufassen:
var keshka = new Parrot("Kesha");
keshka.Speak(); // Kesha sagt: Hallo, Mensch!
keshka.Repeat("Lern, Student!"); // Kesha wiederholt: Lern, Student!
4. Vergleich des Verhaltens der Tiere
| Typ | Methode Speak() | Eigenes Feld | Zusätzliches Verhalten |
|---|---|---|---|
| Animal | Ja (virtual) | Name | — |
| Cat | Ja (override) | Lives | LoseLife() |
| Dog | Ja (override) | — | — |
| Parrot | Ja (override) | — | Repeat(string) |
Wie sieht die Klassenhierarchie im Speicher aus (Blockdiagramm)
Animal (Name)
├── Cat (Lives)
├── Dog
└── Parrot (Repeat)
5. Praxis in unserer Anwendung
Lass uns die Hierarchie-Idee mit einer App verbinden – zum Beispiel haben wir Aufgaben verschiedener Typen:
- Task (Basisklasse): Jede Aufgabe – sie hat einen Titel und einen Erledigt-Status.
- WorkTask (Arbeit): Hat zusätzlich eine Deadline.
- HomeTask (Zuhause): Kann eine Priorität haben ("Sehr wichtig", "So lala").
Starten wir mit der Basisklasse:
public class Task
{
public string Title { get; set; }
public bool IsCompleted { get; private set; }
public Task(string title)
{
Title = title;
}
public virtual void Complete()
{
IsCompleted = true;
Console.WriteLine($"Aufgabe \"{Title}\" erledigt!");
}
}
Jetzt fügen wir eine Arbeitsaufgabe hinzu:
public class WorkTask : Task
{
public DateTime Deadline { get; set; }
public WorkTask(string title, DateTime deadline)
: base(title)
{
Deadline = deadline;
}
public override void Complete()
{
base.Complete();
Console.WriteLine($"Fälligkeitsdatum: {Deadline:d}");
}
}
Und eine Hausaufgabe:
public class HomeTask : Task
{
public string Priority { get; set; }
public HomeTask(string title, string priority)
: base(title)
{
Priority = priority;
}
// Muss Complete nicht überschreiben, wenn das Verhalten aus der Basisklasse reicht
}
Wir legen eine Aufgabenliste in der App an:
List<Task> tasks = new List<Task>
{
new WorkTask("Bericht senden", DateTime.Today.AddDays(2)),
new HomeTask("Geschirr spülen", "Sehr wichtig"),
new Task("Vorlesung über Vererbung lesen")
};
foreach (Task task in tasks)
{
Console.WriteLine($"Aufgabe: {task.Title}");
task.Complete();
}
Erwartete Ausgabe:
Aufgabe: Bericht senden
Aufgabe "Bericht senden" erledigt!
Fälligkeitsdatum: 13.07.2025
Aufgabe: Geschirr spülen
Aufgabe "Geschirr spülen" erledigt!
Aufgabe: Vorlesung über Vererbung lesen
Aufgabe "Vorlesung über Vererbung lesen" erledigt!
Siehst du, wie praktisch: Alle Aufgaben werden zusammen gespeichert, wir verarbeiten sie einheitlich und die Besonderheiten tauchen nur da auf, wo sie gebraucht werden.
6. Typische Fehler bei der Verwendung von Vererbung
Fehler Nr. 1: Versuch, eine Methode zu überschreiben, die nicht als virtual deklariert ist.
Wenn eine Methode in der Basisklasse nicht als virtual markiert ist, kann sie in abgeleiteten Klassen nicht überschrieben werden. Dadurch geht die ganze Flexibilität von Polymorphismus verloren und die Hierarchie wird nutzlos.
Fehler Nr. 2: Vererbung ohne logischen Zusammenhang zwischen Entitäten.
Du solltest keine Vererbung nutzen, wenn die Objekte keinen Sinnzusammenhang haben. Zum Beispiel ist ein Kreis wirklich eine Figur, aber ein Pferd als Fahrzeug – das ist schon etwas weit hergeholt. Ausnahme: spezielle Kontexte (z.B. Mittelalterspiel), wo so eine Verbindung Sinn macht.
Fehler Nr. 3: Zu tiefe Hierarchien.
Wenn die Klassenstruktur zu tief wird (5–6 oder mehr Ebenen), wird der Code schwer lesbar, wartbar und testbar. Das ist ein Signal, dass du Komposition als Alternative zur Vererbung in Betracht ziehen solltest.
Fehler Nr. 4: Basiskonstruktor vergessen.
Wenn du neue Eigenschaften in einer abgeleiteten Klasse hinzufügst, vergisst du leicht, explizit base(...) im Konstruktor aufzurufen. Das kann zu unvollständiger oder falscher Initialisierung des Basisteils des Objekts und zu schwer auffindbaren Bugs führen.
GO TO FULL VERSION