🧬 1. Warum Generics? Typsicherheit & Zero-Cost Abstraction
Vor der Einführung von Generics mussten Collections mit dem Basistyp object arbeiten. Dies führte zu teurem Boxing & Unboxing von Wertetypen (Heap-Allokation) und gefährlichen Laufzeitfehlern (InvalidCastException).
Mit Generics (<T>) erzeugt der C#-Compiler zur Laufzeit für jeden Typ spezialisierten, hochoptimierten Maschinencode:
[Mit Generics] ── int (42) ──► List<int> [Direkter Stack/Array Speicher] ──► Keine Allokation, 100% typsicher!
// Generische Result-Wrapper-Klasse
public sealed class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
private Result(T value) { IsSuccess = true; Value = value; Error = null; }
private Result(string error) { IsSuccess = false; Value = default; Error = error; }
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(string error) => new(error);
}
🔒 2. Generic Constraints (where T : ...)
Standardmäßig weiß der Compiler bei T nur, dass es ein object ist. Mit Constraints schränkst du die erlaubten Typen ein und erhältst Zugriff auf Konstruktoren, Interfaces oder Referenz-/Wertetyp-Eigenschaften:
// Generic Entity Interface mit flexibler ID
public interface IEntity<TId>
{
TId Id { get; set; }
}
// Generic Repository mit mehreren Constraints:
// 1. TEntity muss eine Klasse sein (class)
// 2. TEntity muss IEntity<TId> implementieren
// 3. TEntity muss einen parameterlosen Konstruktor besitzen (new())
public class InMemoryRepository<TEntity, TId>
where TEntity : class, IEntity<TId>, new()
where TId : notnull
{
private readonly Dictionary<TId, TEntity> _storage = new();
public void Add(TEntity entity) => _storage[entity.Id] = entity;
public TEntity? GetById(TId id) => _storage.GetValueOrDefault(id);
public IReadOnlyList<TEntity> GetAll() => _storage.Values.ToList().AsReadOnly();
public bool Remove(TId id) => _storage.Remove(id);
public int Count => _storage.Count;
}
Ein generisches Regal Regal<T> kann jeden Gegenstand lagern. Wenn wir jedoch where T : IPaket, new() verlangen, stellen wir sicher: Jeder gelagerte Gegenstand besitzt zwingend einen Barcode (IPaket) und die Lager-Robotik kann bei Bedarf eine leere Standardbox fabrikneu herstellen (new()).
📑 3. Cheat-Sheet: C# Generic Constraints
| Constraint | Bedeutung & Syntax | Erlaubte Operationen / Nutzen |
|---|---|---|
where T : class |
Typ muss ein Referenztyp sein | Erlaubt null-Prüfungen, verhindert Wertetypen wie int |
where T : struct |
Typ muss ein Wertetyp sein (nicht nullable) | Speichereffizienz auf dem Stack, keine Heap-Allokation |
where T : new() |
Muss öffentlichen parameterlosen Konstruktor haben | Erlaubt Instanziierung im Code via new T() |
where T : ISomeInterface |
Muss das angegebene Interface implementieren | Direkter Aufruf aller Interface-Methoden und Properties auf T |
where T : notnull |
Verbietet Nullable-Typen (z.B. für Dictionary-Keys) | Garantierte Non-Nullability zur Compile-Zeit |
🎯 Aufgaben & Checkliste in Aufgabe.cs
In modernen Clean-Architecture-Projekten abstrahiert ein generisches IRepository<TEntity, TId> den Datenbankzugriff (z.B. Entity Framework Core oder Cosmos DB). So lässt sich die gesamte Persistenzschicht in Unit-Tests kinderleicht durch ein In-Memory-Repository austauschen!