1. Klassifikation der wichtigsten Interfaces
Falls du denkst, Interfaces sind nur Spielzeug für Architekten und "Clean Code"-Fans und man kann im echten Leben darauf verzichten, muss ich dich enttäuschen: Jedes ernsthafte C#-Projekt taucht tief in Interfaces ein. Warum? Weil praktisch jeder Teil der .NET-Standardbibliothek über Interfaces läuft! Ohne sie kannst du weder mit Collections arbeiten, noch Dateien lesen oder sogar Collections mit LINQ filtern.
Interfaces sind das Fundament von Polymorphie und Erweiterbarkeit in .NET und sie bestimmen, wie die ganzen "Bausteine" des Frameworks zusammenpassen.
Die .NET-Standardbibliothek ist vollgestopft mit Interfaces. Damit du darin nicht untergehst, schlage ich folgende (natürlich unvollständige – das Leben reicht nicht, um alles zu erfassen!) Klassifikation vor:
| Kategorie | Interfaces | Wofür braucht man sie? |
|---|---|---|
| Collections | , , , , |
Durchlaufen, Ändern, Zugriff per Index, Arbeit mit Key-Value-Paaren |
| Ressourcenmanagement | |
Freigabe von Ressourcen (Dateien, Verbindungen, Streams) |
| Vergleiche | , , |
Sortierung, Vergleich, Einzigartigkeit von Objekten |
| Serialisierung | |
Objekte in einen Bytestrom verwandeln (und zurück) |
| LINQ und Abfragen | , |
Unterstützung komplexer Abfragen (z.B. für Datenbanken) |
| Asynchronität | , |
Asynchrones Durchlaufen und Freigeben von Ressourcen |
| Events und Benachrichtigungen | , |
Reaktion auf Änderungen von Properties/Collections |
| Datum und Zeit | |
Custom String-Formatierung |
| Datenstrukturen | , |
Tiefes Vergleichen von Collections und Tuples |
| Streams/I/O | , , , |
Arbeit mit Streams, Push/Pull-Benachrichtigungen |
Wichtig! Viele der oben genannten Interfaces haben Typ-Parameter: List<string>. Meistens werden sie in Collections und für Collection-Interfaces verwendet Int[] → List<int>. Mehr dazu gibt's in ein paar Levels, wenn wir uns Collections genauer anschauen.
2. Collection-Interfaces
IEnumerable und IEnumerator – dein Ticket für foreach
Fast jede Collection in .NET implementiert das Interface IEnumerable oder sogar die generische Version IEnumerable<T>. Genau dieses Interface macht den magischen foreach-Syntax möglich:
List<int> zahlen = new List<int> { 1, 2, 3 };
foreach (int n in zahlen) // funktioniert, weil List<int> IEnumerable<int> implementiert
{
Console.WriteLine(n);
}
So sieht das Interface in der minimalistischen Version aus:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerator ist das Interface für den eigentlichen "Durchläufer", der durch deine Collection läuft:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
In echten Aufgaben wirst du sehr oft Parameter übergeben und Werte vom Typ IEnumerable<T> zurückgeben. Zum Beispiel eine Methode, die alle geraden Zahlen aus einem Array zurückgibt:
public IEnumerable<int> GibGeradeZahlen(int[] array)
{
foreach (var x in array)
if (x % 2 == 0) yield return x;
}
Ja, das Schlüsselwort yield macht Magie – es implementiert das Interface für dich, aber dazu später mehr.
ICollection und IList – Collections mit Indexzugriff und Änderung
Wenn du nicht nur durchlaufen, sondern auch Elemente hinzufügen/entfernen oder per Index zugreifen willst, brauchst du spezialisiertere Interfaces:
- ICollection<T> – fügt Methoden wie Add, Remove und die Property Count hinzu.
- IList<T> – erweitert die möglichen Operationen, inklusive Indexzugriff (this[int index]).
public void DruckeErstesElement(IList<string> liste)
{
if (liste.Count > 0)
Console.WriteLine(liste[0]);
}
List<T> implementiert sowohl IEnumerable<T>, ICollection<T> als auch IList<T>. Das ist super praktisch – du kannst damit wie mit einer einfachen Liste arbeiten oder alle Methoden nutzen.
IDictionary<TKey, TValue> – Key-Value-Paare
Wenn du gerne mit Dictionaries arbeitest (und das passiert echt oft!), nutze das Interface IDictionary<TKey, TValue>. Es garantiert, dass du per Key einen Value bekommst und umgekehrt.
public void DruckeAllePaare(IDictionary<string, int> alter)
{
foreach (var paar in alter)
Console.WriteLine($"{paar.Key}: {paar.Value}");
}
Hier kann alter alles Mögliche sein, egal ob Dictionary<string, int> oder SortedDictionary<string, int> – Hauptsache, sie implementieren dieses Interface!
3. Das Interface IDisposable: Ressourcen richtig freigeben
Alles rund um I/O, Dateioperationen, Netzwerkverbindungen, Datenbanken in .NET läuft über das Interface IDisposable. Dieses Interface definiert einen lebenswichtigen Vertrag: Wenn ein Objekt unmanaged Ressourcen hat, muss es nach der Benutzung "aufgeräumt" werden. Ja, das ist der Typ, der den using-Syntax möglich macht:
using (StreamReader reader = new StreamReader("datei.txt"))
{
// Wir arbeiten mit der Datei, und nach using wird sie garantiert geschlossen!
string zeile = reader.ReadLine();
}
Wie sieht das Interface aus? Super simpel:
public interface IDisposable
{
void Dispose();
}
Aber jede "ernsthafte" Bibliothek implementiert es! Mehr zu Best Practices findest du in der offiziellen Doku.
4. Interfaces für Vergleiche und Einzigartigkeit
IComparable<T> und IComparer<T>
Wenn du willst, dass deine Collection von Objekten sortiert werden kann, müssen die Objekte sich untereinander vergleichen können. Dafür gibt's das Interface IComparable<T>:
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Punkte { get; set; }
public int CompareTo(Student? other)
{
// Sortierung nach absteigender Punktzahl
if (other == null) return 1;
return other.Punkte.CompareTo(this.Punkte);
}
}
// Jetzt kann man Studenten sortieren:
var studenten = new List<Student> { ... };
studenten.Sort();
IComparer<T> erlaubt es, den Vergleich außerhalb der Klasse zu definieren – zum Beispiel willst du Studenten mal nach Name, mal nach Punkten sortieren:
public class NameComparer : IComparer<Student>
{
public int Compare(Student? x, Student? y)
{
return string.Compare(x?.Name, y?.Name);
}
}
IEquatable<T> – Vergleich auf Gleichheit
Willst du, dass dein Objekt in einem HashSet<T> (also "einzigartig" in Collections) funktioniert? Implementiere IEquatable<T>:
public class Person : IEquatable<Person>
{
public string Name { get; set; }
public bool Equals(Person? other)
{
if (other == null) return false;
return this.Name == other.Name;
}
}
5. Interfaces für Events und Benachrichtigungen
Wolltest du schon mal, dass dein Programm "mitbekommt", wenn sich Properties eines Objekts ändern? Zum Beispiel soll das UI sich automatisch aktualisieren, wenn der User den Nachnamen ändert? Genau dafür gibt's das Interface INotifyPropertyChanged. Es ist super beliebt in GUI-Anwendungen (z.B. WPF oder Xamarin).
Die Signatur ist simpel:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
Beispiel-Implementierung:
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
}
Jetzt werden alle UI-Bindings zu diesem Objekt automatisch aktualisiert, wenn sich Name ändert. Falls du mehr wissen willst – lies die offizielle Doku.
6. Weitere nützliche Interfaces
Interface IFormattable: Flexibles Formatieren
Wenn du willst, dass dein Objekt hübsch und unterschiedlich als String ausgegeben werden kann (z.B. mit verschiedener Nachkommastellen-Anzahl), implementiere das Interface IFormattable:
public class Temperatur : IFormattable
{
public double Celsius { get; }
public Temperatur(double celsius) => Celsius = celsius;
public string ToString(string? format, IFormatProvider? formatProvider)
{
// Nicht kompliziert machen, einfach 'C' oder 'F' je nach Format anzeigen
if (format == "F")
return $"{Celsius * 9 / 5 + 32} F";
return $"{Celsius} C";
}
}
Jetzt kannst du temp.ToString("F", null) oder temp.ToString("C", null) aufrufen. Deshalb unterstützen DateTime, double, decimal und andere eingebaute Typen flexible Formatierung.
Interfaces für Asynchronität
Mit dem Aufkommen von Asynchronität in C# brauchte man neue Interfaces für die Arbeit in der Async-Welt. Wenn dein Objekt Ressourcen asynchron freigibt – implementiere IAsyncDisposable:
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
Und jetzt schreibst du statt using einfach await using!
Mit asynchronen Enumerationen (await foreach) arbeitet das Interface IAsyncEnumerable<T>, das es erlaubt, Elemente zu durchlaufen, ohne den Main-Thread zu blockieren. Das nutzt man z.B. beim Lesen großer Dateien in Teilen oder beim Arbeiten mit Datenströmen aus dem Internet. Mehr dazu in Level 58 :P
Interfaces für Serialisierung: ISerializable
Wenn du Objekte in einen Bytestrom für Speicherung/Übertragung (z.B. übers Netzwerk) verwandeln willst, bietet .NET das Interface ISerializable. Es wird heute nicht mehr so oft genutzt, weil es bequemere Mechanismen gibt (z.B. über Attribute und fertige Serializer), aber eine Erwähnung ist es wert. Hier die Signatur:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
7. Interfaces in der Praxis: Eine echte Anwendung
Angenommen, du bastelst eine Konsolenanwendung zur Verwaltung von Büchern in einer Bibliothek (hey, irgendwer muss das machen!). Du willst Listen von Büchern ausgeben, sie sortieren, filtern, Daten laden und speichern. Dank deines Interface-Wissens in .NET kannst du:
- Verschiedene Collection-Typen zurückgeben über IEnumerable<Book>, damit der User sich nicht fragen muss, ob es ein Array oder eine Liste ist.
- Sortierung nach verschiedenen Kriterien hinzufügen: Implementiere IComparable<Book> für Sortierung nach Titel und einen eigenen IComparer<Book> für den Autor.
- Effizientes Freigeben von Ressourcen beim Arbeiten mit Dateien über IDisposable.
- Bücher nach Genre oder Autor filtern mit LINQ, das mit allem funktioniert, was IEnumerable<T> implementiert.
- Die Anwendung skalieren – z.B. das Dateispeicher durch eine Datenbank ersetzen, wenn deine Klassen über Interfaces (IBookStorage) und nicht über konkrete Implementierungen arbeiten.
8. Typische Fehler und Besonderheiten
Einer der häufigsten Anfängerfehler ist, konkrete Implementierungen ("List") statt Interfaces ("IEnumerable", "IList") bei Variablen- und Methodendeklarationen zu verwenden. MERKE: Wenn du "auf Interfaces programmierst", bleibt dein Code flexibel und leicht erweiterbar.
Manchmal musst du aufpassen, welches Interface-Level du verwendest – wenn du z.B. eine Methode schreibst, die nur Durchlaufen braucht, nimm IEnumerable<T> statt IList<T>, um keine unnötigen Einschränkungen zu machen.
Noch ein Kniff: Wenn eine Klasse viele Interfaces hat und es Überschneidungen bei Methoden oder Properties gibt, musst du explizite Interface-Implementierung (explicit implementation) nutzen, sonst versteht der Compiler dich nicht.
GO TO FULL VERSION