1. Polymorphismus in der Praxis
In der Programmierung ist Polymorphismus wie eine Universalfernbedienung: Du drückst einen Knopf "Lautstärke+" und steuerst damit TV, Anlage oder Klimaanlage – jedes Gerät reagiert anders, aber das Interface ist das gleiche! Genauso können Objekte verschiedener Typen unterschiedlich auf denselben Methodenaufruf reagieren, wenn diese Methode im gemeinsamen Vorfahren als virtual definiert ist.
In C# zeigt sich Polymorphismus, wenn eine Variable vom Typ Basisklasse (oder Interface) ein Objekt eines beliebigen Nachkommen "halten" kann, und Aufrufe von virtuellen Methoden an so einer Variable führen zur spezifischen, "echten" Implementierung – also der, die das Objekt selbst definiert hat. Das ist die Grundlage für Architekturen, bei denen sich die Logik dynamisch ändert.
Braucht man das überhaupt irgendwo außer im Lehrbuch?
Klar! Eigentlich in jedem Projekt, wo du mit verschiedenen, aber ähnlichen Objekten arbeitest – von Tieren im Zoo bis zu GUI-Elementen, von Event-Handlern bis zu Dokumentenmanagement-Systemen.
- Ermöglicht universelle Algorithmen – Code, der mit abstrakten Entitäten arbeitet, ohne sich um die Details der konkreten Implementierung zu kümmern.
- Garantiert Erweiterbarkeit – füge hundert neue "Tiere", "Figuren", "Handler" hinzu, ohne bestehenden Code anzufassen.
- Reduziert die Abhängigkeit der Programm-Komponenten voneinander (das ist ein wichtiges Thema für Interviews und Architektur!).
2. Grundlegende Syntax und Mechanik
Lass uns unsere Klassen Animal, Dog, Cat nochmal anschauen und verbessern, um Polymorphismus in Aktion zu zeigen. Da wir schon mit der App "Virtueller Zoo" angefangen haben, machen wir damit weiter.
Basisklassen mit virtuellen Methoden
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Virtuelle Methode – kann überschrieben werden
public virtual void MakeSound()
{
Console.WriteLine($"{Name} macht irgendein Geräusch...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} sagt: Wuff-wuff!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} sagt: Miau!");
}
}
Polymorphismus nutzen: Beispiel mit einer Tier-Sammlung
Stell dir vor, wir haben eine Liste von Tieren – Haus- und Wildtiere – und wir wollen, dass alle ein Geräusch machen. Ohne Polymorphismus müssten wir Typprüfungen machen und viel doppelten Code schreiben. Mit Polymorphismus – alles elegant und kompakt!
// Wir erstellen ein Array mit verschiedenen Tieren
Animal[] animals = new Animal[]
{
new Dog("Bobik"),
new Cat("Murka"),
new Dog("Sharik"),
new Cat("Barsik"),
};
// Wir gehen durch das Array und lassen jedes Tier ein Geräusch machen
foreach (var animal in animals)
{
animal.MakeSound(); // Ruft die Methode von Dog oder Cat auf, nicht von Animal!
}
Ergebnis:
Bobik sagt: Wuff-wuff!
Murka sagt: Miau!
Sharik sagt: Wuff-wuff!
Barsik sagt: Miau!
Das ist die ganze Magie: Ein universeller Code – unterschiedliche Ergebnisse je nach tatsächlichem Objekttyp.
Diagramm: Wie Polymorphismus funktioniert
Animal (Basisklasse)
/ \
Dog Cat
Wenn du animal.MakeSound() aufrufst, wobei Animal eine Instanz von Dog oder Cat enthalten kann, entscheidet die .NET-Laufzeitumgebung zur Laufzeit selbst, welche MakeSound()-Methode aufgerufen wird.
3. Typische Aufgaben mit Polymorphismus lösen
Beispiel 1: Universelle Liste, verschiedene Aktionen
Stell dir vor, du baust ein Game. Du hast eine Basisklasse GameObject, davon abgeleitet: Gegner, Freunde, Hindernisse. Alle können sich bewegen, alle haben eine Update()-Methode, aber die Implementierung ist unterschiedlich.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("Der Feind greift an!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("Der Freund hilft!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Gibt aus:
// Der Feind greift an!
// Der Freund hilft!
// Der Feind greift an!
Beispiel 2: Objekte an Methoden übergeben
Du kannst einen Parameter vom Basistyp annehmen und trotzdem jeden Nachkommen verwenden. Das spart richtig viel Aufwand, vor allem wenn später neue Nachkommen dazukommen.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("Und bekommt Futter.");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));
// Ergebnis:
// Rex: Rex sagt: Wuff-wuff!
// Und bekommt Futter.
// Sima: Sima sagt: Miau!
// Und bekommt Futter.
Beachte: So eine Methode ohne Polymorphismus zu schreiben, wäre mega aufwendig – du müsstest den Typ jedes Tiers prüfen, zig ifs schreiben und die richtigen Methoden manuell aufrufen.
Wichtig: Laufzeit-Bindung
Polymorphismus funktioniert, weil Aufrufe von virtuellen Methoden dynamisch passieren, also zur Laufzeit. Das nennt man late binding. Auch wenn wir die Variable als Animal kennen, wird trotzdem die Methode aufgerufen, die im echten Objekt definiert ist.
Wenn die Methode aber nicht als virtual markiert ist, wird immer der Code ausgeführt, der für die Variable (nicht das Objekt) deklariert ist. Also: Spare nicht am Schlüsselwort virtual, wenn du flexibel bleiben willst!
4. Praxis: Unsere App erweitern
Stell dir vor, du willst deinem virtuellen Zoo ein neues Feature geben: Jedes Tier soll nicht nur MakeSound() machen, sondern sich auch bewegen (Move()). Aber jedes Tier macht das anders.
1. Im Basisklasse eine virtuelle Methode hinzufügen
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} macht irgendein Geräusch...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} bewegt sich auf nicht näher bezeichnete Weise...");
}
}
2. In den abgeleiteten Klassen Move() individuell implementieren
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} sagt: Wuff-wuff!");
}
public override void Move()
{
Console.WriteLine($"{Name} rennt dem Stock hinterher.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} sagt: Miau!");
}
public override void Move()
{
Console.WriteLine($"{Name} schleicht auf leisen Pfoten.");
}
}
3. Beide Methoden in einer Sammlung nutzen
Animal[] animals = new Animal[]
{
new Dog("Bim"),
new Cat("Ljusja")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Ergebnis:
Bim sagt: Wuff-wuff!
Bim rennt dem Stock hinterher.
Ljusja sagt: Miau!
Ljusja schleicht auf leisen Pfoten.
In echten Anwendungen kannst du mit so einem Ansatz richtig mächtige und wiederverwendbare Module schreiben. Zum Beispiel ist das in Games die Basis für alle Objekt-Manager – von NPCs bis zu Effekten.
5. Praxisaufgabe: Verschiedene Figuren zeichnen
Schauen wir uns ein Beispiel aus der Grafik an, nicht aus dem Zoo. Wir erstellen eine Basisklasse Shape mit einer virtuellen Draw()-Methode und erweitern sie dann.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Es wird eine undefinierte Figur gezeichnet.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Es wird ein Kreis gezeichnet.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Es wird ein Rechteck gezeichnet.");
}
}
// Sammlung von Figuren
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Hier wird Draw() auf einer Variable vom Typ Shape aufgerufen, aber tatsächlich werden die Methoden von Circle oder Rectangle ausgeführt. In echten Grafikeditoren und Libraries wie WinForms oder WPF läuft das genau so.
6. Allgemeiner Ansatz: Universelle Algorithmen schreiben
Polymorphismus macht deinen Code zu einem flexiblen und erweiterbaren Tool. In einer Sammlung können nicht nur Dog und Cat sein, sondern auch Hamster, Papagei oder was auch immer – und du musst keine einzige neue Zeile schreiben, um sie alle auf die gleiche Art zu behandeln.
Außerdem kannst du problemlos Nachkommen-Objekte an Methoden übergeben, die den Basistyp erwarten:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Name: {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Khoma");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Nutzt die Methode aus Animal
PrintAnimalInfo(dog); // Nutzt die Dog-Version
7. Nützliche Details
Häufige Fragen und Stolperfallen
Viele Anfänger denken, wenn sie ein Dog-Objekt erstellen und dann die Variable als Dog myDog deklarieren, ist das das gleiche wie Animal myDog. Tatsächlich sieht eine Variable vom Typ Animal nur das, was in Animal definiert ist (außer überschriebenen Methoden), während Dog alles sieht: auch Bark() und spezielle Properties.
Wichtig: Wenn eine Methode in der Basisklasse nicht als virtual markiert ist, kann sie nicht überschrieben werden. Versuchst du im Nachfolger ein override für so eine Methode zu schreiben, meckert der Compiler und gibt einen Fehler aus.
Übrigens: Wenn du wirklich eine Methode ersetzen willst, die nicht virtuell ist, nutze das Schlüsselwort new, aber das ist schon ein Thema für Fortgeschrittene (und oft problematisch!).
Warum wird das so gern im Vorstellungsgespräch gefragt?
Weil Polymorphismus wie ein Schweizer Taschenmesser für Programmierer ist: Wenn du weißt, wie du ihn einsetzt, kannst du erweiterbare und wartbare Systeme bauen, und dein Code lebt nicht nur, sondern bleibt auch lange gesund. Zum Beispiel wirst du gebeten, verschiedene Zahlungsarten zu verarbeiten (Klassiker: BankCard, PayPal, Bitcoin) – und man erwartet, dass du ein gemeinsames Interface (oder eine abstrakte Basisklasse) mit der Methode Pay() baust, und dann können Clients Pay(BankCard), Pay(PayPal), Pay(Bitcoin) aufrufen, ohne zu wissen, was intern passiert.
8. Typische Anfängerfehler
Einer der häufigsten Fehler: Du versuchst, eine spezielle Methode der abgeleiteten Klasse über eine Variable vom Basistyp aufzurufen. Zum Beispiel so:
Animal animal = new Dog("Tuzik");
animal.Bark(); // Fehler! Animal hat keine Methode Bark.
Warum geht das nicht? Weil eine Variable vom Typ Animal nur das "sieht", was in Animal deklariert ist, auch wenn da eigentlich ein Dog drinsteckt. Du kannst nur das aufrufen, was in der Basisklasse definiert und (override) überschrieben wurde.
Wenn du trotzdem eine Methode aufrufen willst, die nur bei Dog existiert – musst du einen Cast machen:
Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
dog.Bark();
}
Aber meistens, wenn du das machst, ist irgendwo die Architektur nicht ganz sauber (oder du übertreibst es mit Vererbung).
GO TO FULL VERSION