C# Interface vs. abstrakte Klasse: Unterschiede und Einsatz
7 Min. Lesezeit

Interface und abstrakte Klasse sind die beiden C#-Konzepte, die am häufigsten verwechselt werden. Beide wirken wie „ein Typ, den man nicht instanziieren kann und der anderen als Vorlage dient“, und wenn in der Klausur nach dem Unterschied gefragt wird, besteht die Antwort meist aus einer auswendig gelernten Tabelle. Dabei passt der Unterschied in einen Satz: Ein Interface ist ein Fähigkeitsvertrag („das kann ich“), eine abstrakte Klasse ist eine unfertige Klasse („das bin ich, aber einen Teil muss die abgeleitete Klasse ergänzen“). In diesem Beitrag sehen Sie anhand eines Zahlungssystems, was beide enthalten dürfen, wie Sie sich entscheiden und wie das Thema in Prüfungen abgefragt wird.
Interface: ein Fähigkeitsvertrag
Ein Interface legt fest, welche Member ein Typ nach außen anbietet. Wie diese funktionieren, interessiert es nicht. Denken Sie an die E-Commerce-Bestellung aus dem Beitrag über Konstruktoren. Die Bestellung muss bezahlt werden, aber die Bestellklasse soll nicht wissen müssen, ob das per Karte oder per Überweisung geschieht:
public interface IPaymentMethod
{
string Name { get; }
bool Pay(decimal amount);
}
public interface IRefundable
{
bool Refund(decimal amount);
}Auffällig ist: Die Member haben keinen Rumpf, es steht kein Zugriffsmodifizierer davor (Interface-Member sind standardmäßig public), und ein Feld lässt sich nicht deklarieren. Ein Interface hält keinen Zustand. Es verspricht nur, dass es diese Methoden und Properties gibt.
Abstrakte Klasse: eine unfertige Klasse
Eine abstrakte Klasse ist eine echte Klasse: Sie kann Felder, Konstruktoren und vollständig implementierte Methoden besitzen. Der Unterschied besteht nur darin, dass Sie sie nicht mit new erzeugen können und dass sie einzelne Member als abstract markieren darf, um abgeleiteten Klassen zu sagen: „Diesen Teil schreibst du.“
Zahlungsarten teilen sich einen Ablauf: Betrag prüfen, Gebühr addieren, Zahlung ausführen, Ergebnis protokollieren. Statt das in jeder Klasse zu wiederholen, schreiben wir es einmal:
public abstract class PaymentBase : IPaymentMethod
{
private readonly List<string> _log = new();
protected PaymentBase(string name)
{
Name = name;
}
public string Name { get; }
public IReadOnlyList<string> Log => _log;
// Gemeinsamer Ablauf: jede Zahlungsart durchläuft dieselben Schritte
public bool Pay(decimal amount)
{
if (amount <= 0)
{
Record("Ungültiger Betrag abgelehnt");
return false;
}
decimal total = amount + CalculateFee(amount);
bool result = ProcessPayment(total);
Record($"{total:N2} -> {(result ? "erfolgreich" : "fehlgeschlagen")}");
return result;
}
// Abgeleitete Klassen MÜSSEN das schreiben
protected abstract bool ProcessPayment(decimal total);
// Abgeleitete Klassen DÜRFEN das ändern
protected virtual decimal CalculateFee(decimal amount) => 0m;
protected void Record(string message) => _log.Add($"[{Name}] {message}");
}Drei Dinge darin kann ein Interface nicht: ein privates Feld namens _log, ein protected-Konstruktor und fertiger Code, den alle abgeleiteten Klassen gemeinsam nutzen. Falls Ihnen protected noch nicht vertraut ist, lesen Sie den Beitrag über Zugriffsmodifizierer; die Details zu abstract, virtual und override stehen im Leitfaden zu Vererbung und Polymorphie.
Was darf was enthalten?
| Merkmal | Interface | abstrakte Klasse |
|---|---|---|
| Instanzfelder | Nein | Ja |
| Konstruktor | Nein | Ja (abgeleitete Klassen rufen ihn mit base(...) auf) |
| Methoden mit Rumpf | Nur als Standardimplementierung | Ja |
| Member ohne Rumpf | Ja (der Normalfall) | Ja (mit abstract) |
| Zugriffsmodifizierer | Standardmäßig public |
Alle |
| Wie viele pro Klasse? | Beliebig viele | Genau eine |
Für struct nutzbar? |
Ja | Nein |
Mit new instanziierbar |
Nein | Nein |
Standardimplementierungen in Interfaces
„Interfaces haben keine Rümpfe“ stimmt seit C# 8 nicht mehr ganz. Sie können einem Interface-Member eine Standardimplementierung mitgeben:
public interface IPaymentMethod
{
string Name { get; }
bool Pay(decimal amount);
// Standardimplementierung - ab C# 8
string Summary() => $"Zahlung per {Name}";
}Das Feature gibt es vor allem, damit Sie ein veröffentlichtes Interface um einen Member erweitern können, ohne alle bestehenden Implementierungen zu brechen. Es braucht Unterstützung durch die Laufzeit und steht deshalb im alten .NET Framework nicht zur Verfügung; im modernen .NET funktioniert es einfach. Zwei Grenzen bleiben: Ein Interface kann weiterhin keine Instanzfelder halten und hat keinen Konstruktor. Standardimplementierungen machen aus einem Interface also keine abstrakte Klasse. Interfaces dürfen außerdem statische Member enthalten, doch auf Einsteigerniveau brauchen Sie das nicht.
Einfachvererbung, mehrere Interfaces
Eine C#-Klasse kann nur eine Basisklasse haben, aber beliebig viele Interfaces implementieren. Die Kreditkarte erbt den gemeinsamen Zahlungsablauf und ergänzt die Fähigkeit zur Rückerstattung:
public class CreditCard : PaymentBase, IRefundable
{
private readonly string _cardNumber;
public CreditCard(string cardNumber) : base("Kreditkarte")
{
_cardNumber = cardNumber;
}
protected override bool ProcessPayment(decimal total)
{
// In einem echten Projekt würde hier der Bankdienst aufgerufen
return _cardNumber.Length == 16;
}
protected override decimal CalculateFee(decimal amount) => amount * 0.02m;
public bool Refund(decimal amount)
{
Record($"{amount:N2} erstattet");
return true;
}
}
public class BankTransfer : PaymentBase
{
private readonly string _iban;
public BankTransfer(string iban) : base("Überweisung")
{
_iban = iban;
}
protected override bool ProcessPayment(decimal total) => _iban.StartsWith("DE");
}Ein wichtiges Detail: Niemand ist gezwungen, die Basisklasse zu verwenden. Eine Zahlungsart, die nicht in den gemeinsamen Ablauf passt, implementiert das Interface direkt:
public class GiftCard : IPaymentMethod
{
private decimal _remaining;
public GiftCard(decimal amount)
{
_remaining = amount;
}
public string Name => "Gutschein";
public bool Pay(decimal amount)
{
if (amount <= 0 || amount > _remaining) return false;
_remaining -= amount;
return true;
}
}Mini-Szenario: die Kasse im Onlineshop
Stellen Sie sich eine Checkout-Seite vor: Die Nutzerin wählt eine Zahlungsart, das System versucht die Zahlung und zeigt je nach Ergebnis eine Meldung an. Der Code der Seite muss die konkreten Klassen nicht kennen:
var cartTotal = 1250m;
List<IPaymentMethod> methods =
[
new CreditCard("4111111111111111"),
new BankTransfer("DE00 0000 0000 0000 0000 00"),
new GiftCard(500m)
];
foreach (var method in methods)
{
bool success = method.Pay(cartTotal);
Console.WriteLine($"{method.Summary()} -> {(success ? "genehmigt" : "abgelehnt")}");
if (success && method is IRefundable refundable)
Console.WriteLine($" Erstattung möglich: {refundable.Refund(100m)}");
}Ausgabe:
Zahlung per Kreditkarte -> genehmigt
Erstattung möglich: True
Zahlung per Überweisung -> genehmigt
Zahlung per Gutschein -> abgelehntDie Schleife arbeitet mit drei verschiedenen Klassen und kennt trotzdem nur IPaymentMethod. Kommt morgen eine Zahlungsart „Krypto“ hinzu, bleibt die Schleife unverändert. Die Rückerstattung ist ein eigenes Interface, deshalb lässt sie sich mit is abfragen: Ein Gutschein kann nicht erstattet werden, und das Typsystem sagt uns das. Dieselbe Idee ist die Grundlage der Dependency Injection in ASP.NET Core; im Einstieg in Minimal APIs registrieren und verwenden wir einen Dienst über ein Interface.
.NET selbst steckt voller solcher Verträge: Array.Sort und List<T>.Sort erwarten, dass Ihr Typ IComparable<T> implementiert, damit Elemente verglichen werden können. Wenn Sie wissen möchten, wie das Sortieren darunter abläuft, passt der Beitrag mit Prüfungsfragen zu Sortieralgorithmen als Fortsetzung.
Wann verwenden – und wann nicht?
Wählen Sie ein Interface, wenn:
- nicht verwandte Typen dieselbe Fähigkeit teilen sollen (wie
IComparable<T>oderIDisposable), - eine Klasse mehr als eine Rolle übernehmen muss,
- Sie die echte Klasse in Tests oder per Dependency Injection durch eine Attrappe ersetzen möchten,
- auch
struct-Typen am Vertrag teilnehmen sollen.
Wählen Sie eine abstrakte Klasse, wenn:
- die abgeleiteten Klassen wirklich „eine Art von X“ sind und Felder oder Code teilen,
- ein fester Ablauf wie die Methode
Payoben nur in einzelnen Schritten variiert, - es gemeinsame Daten gibt, die Sie über den Konstruktor erzwingen wollen.
Beides zusammen: Das ist der Aufbau, den Sie in der Praxis am häufigsten sehen. Das Interface definiert den Vertrag, die abstrakte Klasse übernimmt den wiederkehrenden Teil, und der Code außen kennt nur das Interface.
Keines von beiden: Gibt es nur eine Implementierung und keinen konkreten Grund für eine zweite, reicht eine normale Klasse. Reflexartig zu jeder Klasse ein I...-Interface anzulegen, macht Code schwerer lesbar. Für eine Handvoll fester Werte ist ein enum oder ein record ohnehin das bessere Werkzeug.
Häufige Fehler
1. Einen Interface-Member vergessen
Symptom: CS0535: 'CreditCard' does not implement interface member 'IPaymentMethod.Pay(decimal)'. Lösung: Schreiben Sie den Member mit derselben Signatur und als public. Das Gegenstück bei abstrakten Klassen ist CS0534; dort hilft ein override oder Sie markieren Ihre Klasse ebenfalls als abstract.
2. Eine abstrakte Klasse instanziieren wollen
var payment = new PaymentBase("Test");
// CS0144: Cannot create an instance of the abstract type or interface 'PaymentBase'
IPaymentMethod payment2 = new BankTransfer("DE00 ..."); // Richtig: eine konkrete Klasse erzeugen3. Eine Standardimplementierung über eine Klassenvariable aufrufen
Standardimplementierungen werden nicht an die Klasse vererbt; sie sind nur über den Interface-Typ sichtbar:
var card = new CreditCard("4111111111111111");
// card.Summary(); // CS1061: 'CreditCard' does not contain a definition for 'Summary'
IPaymentMethod method = card;
Console.WriteLine(method.Summary()); // Zahlung per Kreditkarte4. Die Basisklasse nach den Interfaces nennen
Mit class CreditCard : IRefundable, PaymentBase erhalten Sie CS1722: Base class 'PaymentBase' must come before any interfaces. Die Reihenfolge lautet immer: erst die Basisklasse, dann die Interfaces.
Typische Prüfungsfragen
Frage 1: Wozu hat eine abstrakte Klasse einen Konstruktor, wenn man sie nicht instanziieren kann?
Wird ein Objekt einer abgeleiteten Klasse erzeugt, läuft zuerst der Konstruktor der Basisklasse. So initialisiert die abstrakte Klasse ihre gemeinsamen Felder selbst. Der Aufruf base("Kreditkarte") im Beispiel ist genau das.
Frage 2: Kann eine abstrakte Klasse ganz ohne abstrakte Member auskommen?
Ja. Das Schlüsselwort abstract bedeutet nur: „Diese Klasse nicht direkt instanziieren.“ Umgekehrt gilt das nicht: Eine Klasse mit einem abstrakten Member muss selbst abstract sein.
Frage 3: Kann eine Klasse von zwei abstrakten Klassen erben?
Nein. C# kennt für Klassen keine Mehrfachvererbung; der Compiler meldet CS1721. Mehrere Interfaces zu implementieren ist dagegen erlaubt.
Frage 4: Was passiert, wenn zwei Interfaces eine Methode mit derselben Signatur deklarieren?
Eine einzige public-Methode erfüllt beide. Sollen sie sich unterschiedlich verhalten, verwenden Sie die explizite Implementierung:
public interface IPrinter { void Start(); }
public interface IScanner { void Start(); }
public class MultiFunctionPrinter : IPrinter, IScanner
{
void IPrinter.Start() => Console.WriteLine("Druck gestartet");
void IScanner.Start() => Console.WriteLine("Scan gestartet");
}
var device = new MultiFunctionPrinter();
((IPrinter)device).Start(); // Druck gestartet
((IScanner)device).Start(); // Scan gestartet
// device.Start(); // Kompiliert nicht: explizite Member sind nur über das Interface sichtbarFrage 5: Darf ein Interface ein Feld deklarieren?
Kein Instanzfeld; der Compiler meldet CS0525. Properties sind erlaubt, weil eine Property eigentlich ein Methodenpaar ist.
Wenn Sie solche Aufgaben auf Papier üben möchten, werfen Sie einen Blick auf die Seite zur Prüfungsvorbereitung.
Häufig gestellte Fragen
Was ist schneller, ein Interface oder eine abstrakte Klasse?
Im normalen Anwendungscode ist der Unterschied nichts, was Sie als Problem messen werden. Entscheiden Sie nach dem Entwurf, nicht nach der Performance: Definieren Sie eine Fähigkeit oder teilen Sie Code?
Kann eine abstrakte Klasse ein Interface implementieren?
Ja, PaymentBase im Beispiel tut genau das. Sie kann die Interface-Member sogar als abstract markieren und den abgeleiteten Klassen überlassen, statt sie selbst zu implementieren.
Machen Standardimplementierungen abstrakte Klassen überflüssig?
Nein. Ein Interface kann weiterhin weder Instanzfelder noch einen Konstruktor enthalten, also keinen Zustand halten. Sobald Sie gemeinsame Daten und gemeinsame Initialisierung brauchen, behält die abstrakte Klasse ihren Platz.
Warum beginnen Interface-Namen mit dem Buchstaben I?
Das ist keine Sprachregel, sondern eine etablierte Namenskonvention in .NET. Wer IPaymentMethod liest, erkennt sofort einen Vertrag; deshalb empfehle ich, dabei zu bleiben.
Verwandte Artikel
C# Vererbung und Polymorphie: virtual, override und new
Leitfaden zu C# Vererbung und Polymorphie: base, Konstruktorverkettung, virtual/override vs. new, sealed, is/as und die Klausurfrage zur Ausgabe.
C# Zugriffsmodifizierer: Türen abschließen oder offen lassen?
Was bedeuten public, private, protected und internal? Wie Getter- und Setter-Methoden Daten sichern, am Beispiel eines Bankkontos.
Einführung in OOP: Warum prozedurale Programmierung endet
Der Weg von prozeduraler zu objektorientierter Programmierung: raus aus dem Spaghetticode, hin zu sauberem und wartbarem Code.