Typ-Design für Leistung
Wann diese Skill verwendet werden sollte
Verwenden Sie diese Skill, wenn:
- Sie neue Typen und APIs entwerfen
- Sie Code auf Leistungsprobleme überprüfen
- Sie sich zwischen class, struct und record entscheiden
- Sie mit Sammlungen und Enumerables arbeiten
Kernprinzipien
- Typen versiegeln – Sofern nicht explizit für Vererbung ausgelegt
- Readonly-Strukturen bevorzugen – Für kleine, unveränderliche Werttypen
- Statische reine Funktionen bevorzugen – Bessere Leistung und Testbarkeit
- Enumeration aufschieben – Nicht materialisieren, bis es nötig ist
- Unveränderliche Sammlungen zurückgeben – An API-Grenzen
Klassen standardmäßig versiegeln
Das Versiegeln von Klassen ermöglicht JIT-Devirtualisierung und kommuniziert die API-Intention.
// DO: Klassen versiegeln, die nicht für Vererbung ausgelegt sind
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
// DO: Records versiegeln (es sind Klassen)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
// DON'T: Ohne Grund unversiegelt lassen
public class OrderProcessor // Kann abgeleitet werden – beabsichtigt?
{
public virtual void Process(Order order) { } // Virtual = langsamer
}
Vorteile:
- Der JIT kann Methodenaufrufe devirtualisieren
- Kommuniziert „Dies ist kein Erweiterungspunkt“
- Verhindert versehentliche Breaking Changes
Readonly-Strukturen für Werttypen
Strukturen sollten readonly sein, wenn sie unveränderlich sind. Dies verhindert defensive Kopien.
// DO: Readonly-Struktur für unveränderliche Werttypen
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// DO: Readonly-Struktur für kleine, kurzlebige Daten
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// DON'T: Veränderbare Struktur (verursacht defensive Kopien)
public struct Point // Nicht readonly!
{
public int X { get; set; } // Veränderbar!
public int Y { get; set; }
}
Wann Strukturen verwenden
| Struktur verwenden, wenn | Klasse verwenden, wenn |
|---|---|
| Klein (typisch ≤16 Byte) | Größere Objekte |
| Kurzlebig | Langlebig |
| Häufig allokiert | Gemeinsame Referenzen benötigt |
| Wertesemantik erforderlich | Identitätssemantik erforderlich |
| Unveränderlich | Veränderbarer Zustand |
Statische reine Funktionen bevorzugen
Statische Methoden ohne Seiteneffekte sind schneller und besser testbar.
// DO: Statische reine Funktion
public static class OrderCalculator
{
public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
var total = items.Sum(i => i.Price * i.Quantity);
return new Money(total, "USD");
}
}
// Verwendung – vorhersagbar, testbar
var total = OrderCalculator.CalculateTotal(items);
Vorteile:
- Keine Vtable-Lookup (schneller)
- Kein versteckter Zustand
- Einfacher zu testen (reine Eingabe → Ausgabe)
- Von Natur aus threadsicher
- Erzwingt explizite Abhängigkeiten
// DON'T: Instanzmethode, die Abhängigkeiten versteckt
public class OrderCalculator
{
private readonly ITaxService _taxService; // Versteckte Abhängigkeit
private readonly IDiscountService _discountService; // Versteckte Abhängigkeit
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// Wovon hängt das eigentlich ab?
}
}
// BESSER: Explizite Abhängigkeiten über Parameter
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// Alle Eingaben sichtbar
}
}
Übertreiben Sie es nicht – Verwenden Sie Instanzmethoden, wenn Sie wirklich Zustand oder Polymorphie benötigen.
Enumeration aufschieben
Materialisieren Sie Enumerables nicht, bevor es nötig ist. Vermeiden Sie übermäßige LINQ-Ketten.
// SCHLECHT: Vorzeitige Materialisierung
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // Materialisiert!
.OrderBy(o => o.CreatedAt) // Weitere Iteration
.ToList(); // Schon wieder materialisiert!
}
// GUT: Bis zum Ende aufschieben
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // Einmalige Materialisierung
}
// GUT: IEnumerable zurückgeben, wenn der Aufrufer möglicherweise nicht alle Elemente benötigt
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// Der Aufrufer entscheidet, wann materialisiert wird
}
Async-Enumeration
Seien Sie vorsichtig mit async und IEnumerable:
// SCHLECHT: Async in LINQ – versteckte Allokationen
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // Task pro Element!
.ToList();
await Task.WhenAll(results);
// GUT: IAsyncEnumerable für Streaming verwenden
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
IEnumerable<Order> orders,
[EnumeratorCancellation] CancellationToken ct = default)
{
foreach (var order in orders)
{
ct.ThrowIfCancellationRequested();
yield return await ProcessOrderAsync(order, ct);
}
}
// GUT: Batch-Verarbeitung für Parallelität
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask vs. Task
Verwenden Sie ValueTask für heiße Pfade, die oft synchron abgeschlossen werden. Für echte I/O verwenden Sie einfach Task.
// DO: ValueTask für gecachte/synchrone Pfade
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // Keine Allokation
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// DO: Task für echte I/O (einfacher, keine Fallstricke)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// Dies trifft immer die Datenbank
return _repository.CreateAsync(cmd);
}
ValueTask-Regeln:
- Ein ValueTask niemals mehr als einmal erwarten
- Niemals
.Resultoder.GetAwaiter().GetResult()vor Abschluss verwenden - Im Zweifel Task verwenden
Span und Memory für Bytes
Verwenden Sie Span<T> und Memory<T> anstelle von byte[] für Low-Level-Operationen.
// DO: Span für synchrone Operationen akzeptieren
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// DO: Memory für asynchrone Operationen akzeptieren
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// DON'T: Array-Allokation erzwingen
public static int ParseInt(string text) // String allokiert
{
return int.Parse(text);
}
Häufige Span-Muster
// Slice ohne Allokation
ReadOnlySpan<char> span = "Hallo, Welt!".AsSpan();
var hallo = span[..5]; // Keine Allokation
// Stack-Allokation für kleine Puffer
Span<byte> buffer = stackalloc byte[256];
// ArrayPool für größere Puffer verwenden
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// Puffer verwenden...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
Sammlungsrückgabetypen
Unveränderliche Sammlungen aus APIs zurückgeben
// DO: Unveränderliche Sammlung zurückgeben
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // Der Aufrufer kann den internen Zustand nicht ändern
}
// DO: Frozen Collections für statische Daten verwenden (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// DON'T: Veränderbare Sammlung zurückgeben
public List<Order> GetOrders()
{
return _orders; // Der Aufrufer kann sie ändern!
}
Interne Mutation ist in Ordnung
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // Intern veränderbar
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // Als IReadOnlyList zurückgeben
}
Richtlinien für Sammlungen
| Szenario | Rückgabetyp |
|---|---|
| API-Grenze | IReadOnlyList<T>, IReadOnlyCollection<T> |
| Statische Lookup-Daten | FrozenDictionary<K,V>, FrozenSet<T> |
| Internes Aufbauen | List<T>, dann als readonly zurückgeben |
| Einzelnes Element oder keins | T? (nullable) |
| Null oder mehr, lazy | IEnumerable<T> |
Kurzreferenz
| Muster | Vorteil |
|---|---|
sealed class | Devirtualisierung, klare API |
readonly record struct | Keine defensiven Kopien, Wertesemantik |
| Statische reine Funktionen | Keine Vtable, testbar, threadsicher |
.ToList() aufschieben | Einmalige Materialisierung |
ValueTask für heiße Pfade | Task-Allokation vermeiden |
Span<T> für Bytes | Stack-Allokation, kein Kopieren |
IReadOnlyList<T> Rückgabe | Unveränderlicher API-Vertrag |
FrozenDictionary | Schnellster Lookup für statische Daten |
Anti-Patterns
// DON'T: Unversiegelte Klasse ohne Grund
public class OrderService { } // Versiegeln Sie es!
// DON'T: Veränderbare Struktur
public struct Point { public int X; public int Y; } // Readonly machen
// DON'T: Instanzmethode, die statisch sein könnte
public int Add(int a, int b) => a + b; // Statisch machen
// DON'T: Mehrere ToList()-Aufrufe
items.Where(...).ToList().OrderBy(...).ToList(); // Ein ToList() am Ende
// DON'T: List<T> aus einer öffentlichen API zurückgeben
public List<Order> GetOrders(); // IReadOnlyList<T> zurückgeben
// DON'T: ValueTask für immer asynchrone Operationen
public ValueTask<Order> CreateOrderAsync(); // Einfach Task verwenden
Ressourcen
- Leistungs-Best Practices: https://learn.microsoft.com/de-de/dotnet/standard/performance/
- Span-Leitfaden: https://learn.microsoft.com/de-de/dotnet/standard/memory-and-spans/
- Frozen Collections: https://learn.microsoft.com/de-de/dotnet/api/system.collections.frozen