1. Vom "reinen Vertrag" zur flexiblen Architektur
Vor C# 8 war ein Interface ein knallharter Vertrag: Willst du ein Interface implementieren – dann implementiere alles, bis zum letzten Komma. Wenn neue Members dazu kamen, mussten alle bestehenden Implementierungen sie sofort hinzufügen, sonst hat der Compiler das Projekt nicht gebaut.
Aber das Leben ist komplizierter. Stell dir vor, du pflegst eine Library, die von hunderten Projekten genutzt wird, und plötzlich musst du eine neue Methode ins Interface packen. Rückwärtskompatibilität nicht kaputt machen? Hier kommen Default-Methoden (Default Interface Methods, DIM) ins Spiel!
Worum geht's?
Default-Methoden erlauben es, eine Implementierung direkt im Interface zu schreiben. Der Vertrag wird flexibler: Wenn eine Klasse das "neue Ding" nicht implementiert, wird die Default-Implementierung genutzt. Das ist wie ein Stunt-Double im Film: Wenn der Schauspieler nicht von der Brücke springen will, macht's der Stuntman.
2. Syntax von Default Interface Methods
Wie deklariert man Methoden mit Implementierung im Interface?
Sieht fast aus wie normale Methoden, nur dass du jetzt den Methodenkörper direkt im Interface schreiben kannst (und musst):
public interface ILogger
{
void Log(string nachricht);
// Neue Methode mit Default-Implementierung!
void LogWarning(string nachricht)
{
Log("[WARNING] " + nachricht);
}
}
Hier hat LogWarning schon eine Implementierung! Jede Klasse, die ILogger implementiert, muss nur Log implementieren, und LogWarning bekommt die Default-Implementierung (außer sie überschreibt sie selbst).
Vergleich: klassische vs. moderne Signatur
| Version | Deklaration im Interface |
|---|---|
| Vor C# 8 | |
| C# 8 und neuer | |
Wichtige Syntax-Details
- Für Methoden mit Implementierung musst du den Methodenkörper in geschweiften Klammern schreiben.
- Default-Methoden dürfen nicht abstract sein.
- Alle Interface-Methoden sind weiterhin implizit public.
- Du kannst auch Properties mit Default-get/set deklarieren (siehe unten).
3. Praktische Beispiele
Beispiel 1. Rückwärtskompatibilität sichern
Angenommen, deine App hat ein Interface zum Speichern von Daten:
public interface ISaveable
{
void Save(string dateipfad);
}
Später willst du Cloud-Speichern hinzufügen. Keine Lust, hundert Klassen zu ändern? Füge eine Default-Methode hinzu!
public interface ISaveable
{
void Save(string dateipfad);
// Neue Methode mit "Default"-Implementierung!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"Speichere in die Cloud {cloudService} (standardmäßig – mache nichts)");
}
}
Jetzt können alle alten Klassen automatisch "in die Cloud speichern" (auch wenn sie erstmal nur eine Nachricht ausgeben).
Beispiel 2. Logger-Interface erweitern
Vorher hatten wir ein einfaches Logging-Interface:
public interface ILogger
{
void Log(string nachricht);
}
Fügen wir eine Default-Methode fürs Error-Logging hinzu:
public interface ILogger
{
void Log(string nachricht);
void LogError(string nachricht)
{
Log("[ERROR] " + nachricht);
}
}
Eine Klasse, die ILogger implementiert, muss LogError nicht implementieren – die Default-Version läuft:
public class ConsoleLogger : ILogger
{
public void Log(string nachricht)
{
Console.WriteLine(nachricht);
}
// LogError nicht implementiert – Default-Implementierung wird genutzt!
}
ILogger logger = new ConsoleLogger();
logger.Log("Alles gut!");
logger.LogError("Oh oh, irgendwas ist schief gelaufen!"); // Ruft die Default-Implementierung auf
Beispiel 3. Default-Methoden + erweiterbare App
Deine App unterstützt verschiedene Export-Typen: Datei, DB, Netzwerk. Das Interface:
public interface IExporter
{
void Export(string daten, string ziel);
// Neue Funktion – Export ins Archiv
void ExportToArchive(string daten, string archivPfad)
{
Console.WriteLine("Archivierung wird standardmäßig nicht unterstützt.");
}
}
Plugins von anderen Entwicklern funktionieren weiter, auch wenn sie von der neuen Methode nichts wissen.
4. Wie funktioniert der Aufruf von Default-Methoden?
Szenario "alte Klasse – neues Interface"
Wenn eine Klasse die Default-Methode nicht implementiert, wird beim Aufruf über die Interface-Referenz die Interface-Implementierung genutzt. Wenn sie überschrieben wird – die eigene.
public class FileExporter : IExporter
{
public void Export(string daten, string ziel)
{
Console.WriteLine("Speichere in Datei...");
}
// ExportToArchive nicht implementiert – Default-Ausgabe wird genutzt
}
IExporter exporter = new FileExporter();
exporter.Export("daten", "file.txt"); // Eigene Implementierung von FileExporter
exporter.ExportToArchive("daten", "file.zip"); // Default-Implementierung!
Szenario "Klasse überschreibt Default-Methode"
public class AdvancedExporter : IExporter
{
public void Export(string daten, string ziel)
{
Console.WriteLine("Speichere im Advanced-Modus...");
}
public void ExportToArchive(string daten, string archivPfad)
{
Console.WriteLine("Archivierung wird unterstützt!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("daten", "file.zip"); // Jetzt wird die Klassen-Implementierung aufgerufen!
5. Was kann man noch mit Default Interface Methods machen?
Properties und Events mit Default
Du kannst Properties mit Default-Implementierung deklarieren, wenn sie einen get- oder set-Body haben:
public interface IHasId
{
// Gibt automatisch 42 zurück, solange nicht überschrieben
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
Default-Methoden im Interface-Code aufrufen
Innerhalb des Interfaces können Default-Methoden und andere Members sich gegenseitig aufrufen:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Einschränkungen und Besonderheiten von Default Interface Methods
Darf man Felder, Konstruktoren deklarieren?
Nope. Auch mit Default-Methoden ist ein Interface immer noch keine Klasse. Felder, Konstruktoren, Destruktoren sind nicht erlaubt.
Darf man base im Interface nutzen?
Ja, aber mit Einschränkungen. Innerhalb einer Default-Methode kann man die Methode des Parent-Interfaces explizit aufrufen:
public interface IBase
{
void Greet() => Console.WriteLine("Hallo von IBase");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("Hallo aus IDerived!");
IBase.Greet(this); // Expliziter Aufruf der Methode vom Basis-Interface
}
}
Aber das braucht man selten in Standard-Szenarien.
Was passiert bei Konflikt von Default-Implementierungen?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// Klasse implementiert Foo nicht explizit:
public class C : IA, IB { }
// Compiler-Fehler: Unklar, welche Implementierung genommen werden soll!
7. Typische Fehler, Einschränkungen und Besonderheiten
Lustiger typischer Fehler:
Manche versuchen, nach dem Kennenlernen von DIM Felder im Interface zu deklarieren – aber das geht immer noch nicht. Und wenn du versuchst, eine statische Default-Methode zu machen – vor C# 8 geht das nicht. (Ab Version 8 kann man statische Methoden im Interface haben, aber Default-Implementierungen für statische Methoden sind ein anderes Thema.)
Besonderheit: Diamond Problem ("Diamant-Problem")
Wenn deine Klasse zwei Interfaces mit derselben Default-Methode implementiert, musst du diese Methode explizit selbst implementieren:
public class ConflictClass : IA, IB
{
public void Foo() // Du musst selbst wählen, welche Variante!
{
// Expliziter Aufruf der gewünschten Implementierung via Interface (wenn nötig)
((IA)this).Foo();
// oder
((IB)this).Foo();
}
}
Don't overdo it!
Default-Methoden retten die Rückwärtskompatibilität, aber wenn du sie zu oft nutzt, wird die Architektur schnell "dirty", weil Logik im Interface landet. Halte das Wichtige lieber in den Klassen und nutze das Interface wirklich als "Vertrag".
GO TO FULL VERSION