1. Einführung
Lass uns zu unserem Zoo zurückkehren. Wir haben eine Basisklasse Animal (Tier) und davon abgeleitete Klassen: Dog (Hund), Cat (Katze), Fish (Fisch).
In den vorherigen Vorlesungen haben wir der Animal Klasse eine virtual-Methode MakeSound() hinzugefügt:
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public virtual void MakeSound() // Virtuelle Methode
{
Console.WriteLine("Irgendein Tier macht ein Geräusch."); // Standardimplementierung
}
public void Sleep() // Normale Methode
{
Console.WriteLine($"{Name} schläft.");
}
}
public class Dog : Animal
{
public override void MakeSound() // Überschreiben für Hund
{
Console.WriteLine("Wuff-wuff!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Überschreiben für Katze
{
Console.WriteLine("Miau!");
}
}
Das funktioniert super! Wenn wir Dog oder Cat erstellen und MakeSound() aufrufen, hören wir deren individuellen Sound. Aber was passiert, wenn wir einfach ein Animal erstellen?
Animal genericAnimal = new Animal();
genericAnimal.Name = "Unbekanntes Wesen";
genericAnimal.MakeSound(); // Gibt aus: "Irgendein Tier macht ein Geräusch."
Sieht logisch aus. Aber manchmal ergibt die Standardimplementierung einfach keinen Sinn. Was, wenn Animal kein konkretes Tier ist, sondern eher ein Konzept? "Tier" an sich macht kein bestimmtes Geräusch, das machen erst die konkreten Tierarten. Oder stell dir vor, wir haben eine Klasse Shape (Figur) mit einer Methode CalculateArea() (BerechneFläche). Welche Fläche soll eine allgemeine Figur berechnen? Keine! Einen Flächeninhalt haben Kreis, Quadrat, aber nicht die abstrakte "Figur".
In solchen Fällen, wenn die Basisklasse keine sinnvolle Standardimplementierung liefern kann (oder soll), aber alle abgeleiteten Klassen verpflichtet, diese Methode zu implementieren, kommen abstrakte Methoden und abstrakte Klassen ins Spiel.
2. Abstrakte Klassen: Wenn der Bauplan noch kein Haus ist
Stell dir vor, du bist Architekt und entwirfst einen Bauplan für ein Standardhaus. Aber nicht für irgendein Haus, sondern für ein "Konzept-Haus". Es gibt gemeinsame Merkmale: Wände, Dach, Fundament. Aber du weißt noch nicht, ob es ein einstöckiges Cottage oder ein Wolkenkratzer wird. Manche Teile des Plans sind konkret (z.B. Deckenhöhe im Erdgeschoss), andere sind nur angedeutet (z.B. "Anzahl der Stockwerke", die später festgelegt wird).
In der C#-Welt nennt man so ein "Konzept-Haus" eine abstrakte Klasse.
Abstrakte Klasse – das ist eine Klasse, die mit dem Schlüsselwort abstract markiert ist.
public abstract class Animal // Jetzt ist Animal eine abstrakte Klasse
{
// ...
}
Das wichtigste Merkmal abstrakter Klassen:
- Man kann sie nicht direkt instanziieren. Du kannst nicht einfach new Animal() schreiben. Warum? Weil Animal jetzt etwas Unbestimmtes ist, ein Konzept. Du kannst kein "Konzept-Haus" bauen, sondern nur ein konkretes Cottage oder einen Wolkenkratzer.
Wenn du new Animal() versuchst, gibt dir der Compiler sofort einen auf die Finger:
Das ist eine sehr wichtige Einschränkung!Cannot create an instance of the abstract type or interface 'Animal' (Es ist nicht möglich, eine Instanz des abstrakten Typs oder Interface 'Animal' zu erstellen) - Sie können abstrakte Member enthalten. Und das ist das Spannendste!
3. Abstrakte Methoden: Vertrag ohne Implementierung
Wenn die abstrakte Klasse das "Konzept-Haus" ist, dann ist die abstrakte Methode der Teil des Bauplans, der als "mach das" markiert ist, aber ohne konkrete Anweisung "wie". Zum Beispiel "Fundament bauen" – das ist ein Muss für jedes Haus, aber die genauen Maße und Materialien hängen vom Haustyp ab.
Abstrakte Methode – das ist eine Methode, die:
- Mit dem Schlüsselwort abstract markiert ist.
- Keinen Rumpf hat (also keinen Codeblock {}). Sie endet mit einem Semikolon ;.
- Nur innerhalb einer abstrakten Klasse deklariert werden kann.
Lass uns unsere Methode MakeSound() abstrakt machen:
public abstract class Animal // Wir machen Animal abstrakt
{
public string Name { get; set; }
public int Age { get; set; }
public abstract void MakeSound(); // Hier ist sie, die abstrakte Methode! Kein Rumpf!
public void Sleep() // Diese Methode bleibt normal, "konkret"
{
Console.WriteLine($"{Name} schläft.");
}
}
Schau dir an, wie sich MakeSound() verändert hat! Sie hat keine geschweiften Klammern und keine Standardimplementierung mehr. Jetzt sagt sie einfach: "Jedes Tier muss einen Sound machen können. Wie genau – das sollen die entscheiden, die von mir erben."
Wichtiges Gesetz: Wenn deine Klasse von einer abstrakten Klasse erbt und selbst nicht abstrakt ist, muss sie (mit override) alle abstrakten Methoden der Basisklasse überschreiben. Das ist keine Option, das ist Pflicht, Vertrag! Der C#-Compiler ist da sehr streng. Wenn du es vergisst, erinnert er dich sofort daran:
public class Dog : Animal // Normale, nicht-abstrakte Klasse
{
// KOMPILIERUNGSFEHLER!
// 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
// (Klasse 'Dog' implementiert das geerbte abstrakte Member 'Animal.MakeSound()' nicht)
// Der Compiler erwartet von uns MakeSound() mit override!
}
Um den Fehler zu beseitigen, müssen Dog und Cat unbedingt MakeSound() überschreiben:
public class Dog : Animal
{
public override void MakeSound() // Muss überschrieben werden!
{
Console.WriteLine("Wuff-wuff!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Auch hier!
{
Console.WriteLine("Miau!");
}
}
Vergleich virtual, abstract und override
| Eigenschaft | virtual Methode | abstract Methode | override Schlüsselwort |
|---|---|---|---|
| Wo erlaubt? | In normalen oder abstrakten Klassen | Nur in abstrakten Klassen | In abgeleiteten (Kind-)Klassen |
| Methode mit Rumpf? | Hat Rumpf (Standardimplementierung) | Kein Rumpf (endet mit ;) | Hat Rumpf (neue Implementierung) |
| Zweck | Bietet Standardimplementierung, erlaubt aber Überschreiben in Kindklassen | Vertrag: Kindklassen müssen eigene Implementierung liefern | Bietet spezifische Implementierung für virtual oder abstract Methode der Basisklasse |
| Instanziierung der Basisklasse? | Ja (wenn Basisklasse nicht abstrakt ist) | Nein (wenn Basisklasse abstrakt ist) | N/A (bezieht sich auf Methode, nicht auf Klasse) |
| Pflicht für Kindklasse | Optional überschreiben (override) | Muss überschrieben werden (override), wenn Kindklasse nicht abstrakt ist | N/A |
4. Polymorphismus in Aktion mit abstrakten Methoden
Jetzt wird's richtig spannend! Wir wissen schon, dass Polymorphismus es uns erlaubt, mit Objekten verschiedener abgeleiteter Klassen über eine gemeinsame Referenz auf die Basisklasse zu arbeiten. Und das funktioniert auch dann, wenn die Basisklasse abstrakt ist!
Auch wenn wir keine Instanz von Animal direkt erstellen können (denk dran, new Animal() gibt einen Fehler), können wir den Typ Animal als Referenz auf Objekte der abgeleiteten Klassen verwenden. Das ist mega mächtig!
Lass uns mit unserem Zoo weitermachen. Stell dir vor, wir haben einen Bauernhof, auf dem verschiedene Tiere leben. Jedes Tier soll seinen eigenen Sound machen.
using System;
// Abstrakte Klasse Animal
public abstract class Animal
{
public string Name;
public int Age;
public Animal(string name, int age) { Name = name; Age = age; }
public abstract void MakeSound();
public void Sleep() { Console.WriteLine($"{Name} schläft."); }
}
public class Dog : Animal
{
public Dog(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Wuff-wuff!"); }
}
public class Cat : Animal
{
public Cat(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Miau!"); }
}
public class Fish : Animal
{
public Fish(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Blubb-blubb"); }
}
class Program
{
static void Main()
{
Animal[] animals = {
new Dog("Sharik", 3),
new Cat("Murzik", 5),
new Fish("Nemo", 1)
};
foreach (Animal animal in animals)
{
Console.WriteLine($"\nHallo, ich bin {animal.Name}, ich bin {animal.Age} Jahre alt.");
animal.MakeSound();
animal.Sleep();
}
}
}
Was passiert in diesem Code?
- Wir haben Animal als abstract class deklariert. Das sagt dem Compiler: "Diese Klasse ist eine Vorlage, sie selbst kann nicht erstellt werden, aber man kann von ihr erben".
- Wir haben public abstract void MakeSound(); in Animal deklariert. Das sagt: "Jede Klasse, die von Animal erbt (und nicht selbst abstrakt ist), muss die Methode MakeSound() implementieren". Das ist unser Vertrag!
- Dog, Cat und Fish halten sich brav an diesen Vertrag und überschreiben MakeSound() mit ihrer eigenen Implementierung. Hätten wir das bei einer vergessen, hätte der Compiler unseren Code nicht akzeptiert.
- In der Main-Methode erstellen wir ein Array Animal[]. Obwohl Animal abstrakt ist, kann das Array Referenzen auf Objekte der abgeleiteten Klassen (Dog, Cat, Fish) speichern, weil sie auch Animal sind!
- Wenn wir mit foreach durch das Array gehen und animal.MakeSound() aufrufen, weiß C# dank Polymorphismus genau, welche MakeSound() Methode aufgerufen werden soll: Dog.MakeSound(), Cat.MakeSound() oder Fish.MakeSound(). Es wird die Methode des tatsächlichen Objekttyps aufgerufen, auf den die Animal-Referenz gerade zeigt, nicht die der Referenz selbst. Das ist die ganze Magie des Polymorphismus!
- animal.Sleep() ruft die konkrete Implementierung aus der Basisklasse Animal auf, weil diese Methode nicht als virtual oder abstract markiert und nicht in den Kindklassen überschrieben wurde.
5. Wozu braucht man das im echten Leben?
"Okay, Zoo, Tiere... Aber wo brauche ich das, wenn ich mal eine echte Anwendung für eine Bank oder einen Shop schreibe?" – fragst du dich vielleicht. Und das ist eine super Frage! Abstrakte Klassen und Methoden sind ein mächtiges Werkzeug, um flexible und erweiterbare Systeme zu designen.
Erzwingen der Vertragserfüllung: Das ist der Hauptvorteil. Stell dir vor, du entwickelst ein Framework für Bezahlsysteme. Du hast eine Basisklasse abstract class PaymentProcessor (Zahlungsabwickler). Und du weißt genau, dass jeder Zahlungsabwickler ProcessPayment() (ZahlungVerarbeiten), RefundPayment() (ZahlungZurückgeben) und CheckStatus() (StatusPrüfen) können muss. Aber wie das bei PayPal, Kreditkarte oder Bitcoin läuft – ist komplett unterschiedlich.
Du deklarierst diese Methoden als abstract in PaymentProcessor.
public abstract class PaymentProcessor
{
public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
public abstract bool RefundPayment(string transactionId);
public abstract string CheckStatus(string transactionId);
// ... andere Methoden, die auch konkret sein können, z.B. Logging
public void LogTransaction(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
public class PayPalProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Hier komplexe Logik für PayPal API
Console.WriteLine($"PayPal: verarbeite {amount} {currency}...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "Abgeschlossen"; }
}
public class CreditCardProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Hier Logik für Bank-Acquirer
Console.WriteLine($"CreditCard: {amount} {currency} von Karte {cardNumber.Substring(0,4)}XXXX...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "In Bearbeitung"; }
}
Jetzt muss jeder Entwickler, der einen neuen Zahlungsabwickler (z.B. BitcoinProcessor) bauen will, alle diese drei Methoden implementieren. Er kann nicht versehentlich RefundPayment() vergessen, weil der Compiler ihn nicht lässt! Das sorgt für Konsistenz im System.
Flexibilität und Erweiterbarkeit: Du kannst Code schreiben, der mit PaymentProcessor arbeitet, ohne zu wissen, welche konkrete Implementierung verwendet wird. Zum Beispiel rufst du im Shop-Code einfach currentProcessor.ProcessPayment() auf, und das System nimmt je nach gewählter Zahlungsmethode den richtigen Prozessor. Morgen kommt eine neue Zahlungsmethode dazu – du schreibst einfach eine neue Klasse, erbst von PaymentProcessor, implementierst die abstrakten Methoden, und dein Shop-Code muss nicht geändert werden!
Keine leeren Implementierungen: Wenn wir virtual Methoden statt abstract verwenden würden, müssten wir eine leere Standardimplementierung geben, was irreführend sein kann. abstract sagt klar: "Hier gibt es keine Implementierung und es darf auch keine geben – geh zu den Nachfolgern!"
Bessere Code-Architektur: Abstrakte Klassen helfen, allgemeine und spezifische Logik sauber zu trennen. Allgemeines (z.B. LogTransaction in PaymentProcessor) lebt in der Basisklasse, Spezielles (wie ProcessPayment) in den Kindklassen. Das macht den Code lesbarer, wartbarer und testbarer. So können Framework-Designer "Erweiterungspunkte" definieren, die die Nutzer ihrer Bibliotheken ausfüllen müssen.
6. Häufige Fehler und Stolperfallen
Fehler Nr. 1: Versuch, eine Instanz einer abstrakten Klasse zu erstellen.
Das ist immer ein Compilerfehler. Eine abstrakte Klasse ist ein Konzept, kein konkretes Objekt. Du kannst sie als Referenztyp verwenden, aber nicht new Animal() schreiben.
Fehler Nr. 2: Abstrakte Methode nicht überschrieben.
Wenn die Kindklasse nicht abstrakt ist, muss sie alle abstract-Methoden der Basisklasse implementieren. Sonst gibt's einen Compilerfehler. Der einzige Weg drumherum: Die Kindklasse selbst auch abstrakt machen – aber das willst du meistens nicht.
Fehler Nr. 3: Verwechslung von abstract und virtual.
abstract zwingt zum Überschreiben, hat keinen Rumpf. virtual gibt eine Standardimplementierung und kann überschrieben werden. Du kannst keine abstract-Methode mit Rumpf oder virtual-Methode ohne Rumpf deklarieren – das ist ein Syntaxfehler.
Fehler Nr. 4: Verwendung von new statt override.
Wenn du nicht override verwendest, sondern einfach eine Methode mit gleichem Namen in der Kindklasse schreibst, überschreibst du nicht, sondern versteckst die Methode der Basisklasse. Das führt zu unerwartetem Verhalten bei polymorphen Aufrufen: Es wird die Methode der Basisklasse aufgerufen.
public class Base
{
public void DoSomething() { Console.WriteLine("Base"); }
}
public class Derived : Base
{
public new void DoSomething() { Console.WriteLine("Derived"); }
}
Base obj = new Derived();
obj.DoSomething(); // Gibt aus: Base
GO TO FULL VERSION