1. Einführung
Bevor wir in den Code springen, lass es uns einfach erklären. Erinnerst du dich, dass das einfache Erzeugen eines Objekts in .NET (und besonders eines Tasks!) Speicher kostet und etwas CPU-Zeit? Stell dir nun ein asynchrones API vor, das in der Hälfte der Fälle das Ergebnis sofort zurückgibt (z. B. aus dem Cache), und in der anderen Hälfte auf die Datenbank zugreift, wodurch die Operation wirklich asynchron wird und ein Task nötig ist. Das kommt oft vor — z. B. bei Dateicaches, Netzwerk-APIs, Objektpools und anderen "asynchronen, aber manchmal sofortigen" Szenarien.
Wenn wir immer ein Task zurückgeben, entstehen auch bei sofortigen Ergebnissen unnötige Objekte. Was wäre, wenn wir das Ergebnis ohne Task zurückgeben könnten, falls es schon fertig ist? Genau dafür wurde ValueTask erfunden.
Fakt: die Standard-Task.CompletedTask und Task.FromResult(…) sparen zwar Ausführungszeit, erzeugen aber gemeinsame Objekte, die für hochbelastete Szenarien nicht ideal sein können.
Was ist ein ValueTask?
ValueTask ist eine spezielle Struct-Wrapper, die entweder ein bereits fertiges Ergebnis oder (falls die Operation wirklich asynchron ist) den Task selbst repräsentieren kann. Einfach gesagt ist es ein "Paket", das entweder direkt das Ergebnis oder eine Referenz auf einen Task enthält.
Es gibt zwei Hauptvarianten:
- ValueTask — "sinnlos" (gibt keinen Wert zurück, wie Task)
- ValueTask<TResult> — Wrapper für einen Wert (Analog zu Task<TResult>)
Vergleich: Task vs ValueTask
| Typ | Anzahl Allocations | Kann synchron abgeschlossen sein | Wird üblicherweise verwendet |
|---|---|---|---|
|
Eine (Heap) | Ja/Nein | Praktisch immer |
|
Keine/eine | Ja/Nein | Optimierung |
|
Keine/eine | Ja/Nein | Optimierung |
Wann man ValueTask verwenden sollte
Es gibt eine goldene Regel: Wenn du einen normalen asynchronen Methodenschreibstil hast, der das Ergebnis immer erst nach einer async-Operation zurückgibt, benutze Task. Das ist einfach, sicher und für alle verständlich.
Nutze ValueTask, wenn:
- das Ergebnis synchron verfügbar sein kann (z. B. aus Cache, Pool, Speicher) und du Allocations sparen willst.
- die asynchrone Operation nicht sehr häufig auftritt (ansonsten gehen die Vorteile in der Komplexität und den Kopierkosten der Struct verloren).
Achtung! Wenn du immer ein asynchrones Ergebnis zurückgibst — nimm ruhig Task. Wenn du dein API "super-optimiert" für häufige sofortige Ergebnisse machen willst — dann ValueTask.
2. Synchrones oder asynchrones Ergebnis
Schauen wir uns eine Funktion an, die einen Benutzer nach Name sucht. Wenn der Benutzer im Cache ist — geben wir ihn sofort zurück; wenn nicht — laden wir asynchron aus der "Datenbank":
// Wir modellieren unseren Benutzer
public class User
{
public string Name { get; set; }
}
// Unser Cache (sehr einfach)
private readonly Dictionary<string, User> _localCache = new();
public async ValueTask<User> FindUserAsync(string name)
{
// Prüfen des lokalen Caches
if (_localCache.TryGetValue(name, out var user))
{
// Ergebnis ist sofortig — keine Task-Allocation!
return user;
}
// Hier eine angeblich lange asynchrone Operation (z. B. aus der Datenbank)
user = await LoadUserFromDbAsync(name);
// In den Cache für die Zukunft legen
_localCache[name] = user;
return user;
}
private async Task<User> LoadUserFromDbAsync(string name)
{
// Verzögerung simulieren
await Task.Delay(500);
return new User { Name = name };
}
Wichtig: Im Cache-Hit-Fall geben wir das normale Ergebnis zurück — es wird kein Task alloziert! Nur wenn das Ergebnis "erarbeitet" werden muss, erzeugen wir tatsächlich eine asynchrone Task.
Wie ValueTask innen aufgebaut ist
ValueTask kann intern entweder einen fertigen Wert oder eine Referenz auf einen Task enthalten:
ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));
Beim Verwenden von await kümmert sich der Compiler selbst: Wenn das Ergebnis sofort da ist, werden keine zusätzlichen Objekte erzeugt.
Wichtiges zu await und ValueTask
async-Methoden arbeiten gut mit await auf ValueTask:
public async ValueTask PingAsync()
{
// ...
await Task.Delay(10);
}
Aber wenn du eine Instanz von ValueTask speicherst und später darauf wartest, denk daran: du darfst nicht denselben Wert mehr als einmal awaiten. Task kann mehrfach erwartet werden, ValueTask nicht.
Fehler: erneutes awaiten eines ValueTask
ValueTask<int> task = ComputeAsync();
// Das ist OK
int a = await task;
// Das ist ein Fehler! Ein zweites await auf derselben ValueTask-Instanz ist nicht erlaubt:
// int b = await task; // NICHT ERLAUBT!
3. Wir überarbeiten einen Teil unserer Konsolenanwendung
Angenommen, wir entwickeln jetzt einen Mini-Book-Reader: Teile der Texte sind bereits im Cache, der Rest wird asynchron nachgeladen. Implementieren wir eine optimierte Methode, die die erste Zeile eines Buches mit ValueTask zurückgibt:
private readonly Dictionary<string, string> _bookCache = new();
public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
if (_bookCache.TryGetValue(title, out var bookText))
{
var firstLine = bookText.Split('\n')[0];
return firstLine;
}
// Angenommen, asynchroner Download des Buches
var downloadedBook = await DownloadBookTextAsync(title);
_bookCache[title] = downloadedBook;
return downloadedBook.Split('\n')[0];
}
private async Task<string> DownloadBookTextAsync(string title)
{
// Verzögerung simulieren (z. B. Download aus dem Internet)
await Task.Delay(1000);
return $"Book: {title}\nThis is the first line.\nSecond line...";
}
So spart ValueTask die Erstellung eines Tasks, wenn das Buch bereits im Cache ist.
4. Nützliche Feinheiten
Marker: wann du kein ValueTask verwenden solltest
Wenn dein API immer asynchron ist (z. B. immer Netzwerkommunikation) — benutze Task, das ist einfacher und sicherer.
Wie man ValueTask in Task und umgekehrt wandelt
Manchmal passt ValueTask nicht in ein API, das ein Task erwartet. Verwende .AsTask():
ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();
Wenn du aus einem Task einen ValueTask machen willst — erstelle einfach einen ValueTask aus dem Task:
Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);
Vergleich: ValueTask vs Task
| Eigenschaft | |
|
|---|---|---|
| Erneutes await | Erlaubt | Nicht erlaubt |
| Allocations bei schnellen Antworten | Ja | Nein (wenn Ergebnis sofort) |
| Interface-Kompatibilität | Weit verbreitet | Erfordert zusätzliche Wrapper |
| Anwendbarkeit | Überall | Nur zur Optimierung |
| Pooling | Verwendet gemeinsamen Pool | Nein, ist eine Struct |
| Einfachheit | Einfach | Komplexer |
Einsatz von ValueTask im echten Leben
public async ValueTask<string> GetMessageAsync(int id)
{
if (_messageCache.TryGetValue(id, out var value))
return value;
var result = await LoadMessageFromDbAsync(id);
_messageCache[id] = result;
return result;
}
Im Vorstellungsgespräch anwendbar: Wenn man dich fragt, wie man die Performance eines asynchronen, cache-basierten APIs noch weiter verbessern kann — erwähne ValueTask.
Beispiele zur Kompatibilität mit LINQ und IAsyncEnumerable<T>
Wenn du ValueTask mit asynchronem LINQ (IAsyncEnumerable<T>) verwenden willst, wird das in .NET unterstützt (z. B. die Methode ToListAsync):
public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
// Simulieren, dass ein Teil der Zahlen bereits berechnet ist und ein Teil asynchron kommt
// ... (Beispiel aus Platzgründen ausgelassen)
return new List<int> { 2, 3, 5, 7, 11 };
}
5. Fazit und Implementierungsbesonderheiten
- Verwende ValueTask zur Optimierung, wenn ein großer Teil der Zeit dein Ergebnis sofort bereitsteht (z. B. aus Cache).
- Verwende ValueTask nicht "einfach so" — das macht den Code komplizierter und führt zu Fehlern.
- Await-ein ValueTask nicht mehrfach. Wenn nötig, konvertiere ihn in einen Task.
- Die gesamte Kompatibilitätslogik mit Task läuft über .AsTask().
- Denk dran: ValueTask ist ein struct und kann sich beim Kopieren anders verhalten.
6. Typische Fehler beim Arbeiten mit ValueTask
Fehler Nr.1: mehrfaches await auf derselben ValueTask-Instanz. ValueTask ist eine Struct und darf nicht zweimal await-et werden. Ein zweites await führt zu einer Exception oder zu falschem Verhalten.
Fehler Nr.2: Verwendung von ValueTask ohne Prüfung der Kompatibilität. Manche Drittanbieter-APIs erwarten ein Task, und direktes Weiterreichen eines ValueTask kann Probleme machen.
Fehler Nr.3: Einsatz von ValueTask ohne Notwendigkeit. Wenn das Ergebnis immer asynchron ist, macht ValueTask den Code nur komplizierter ohne wirklichen Nutzen.
Fehler Nr.4: Kopieren der ValueTask-Struct. Eine kopierte Struct kann beim await überraschend anders reagieren, arbeite möglichst mit dem Original.
GO TO FULL VERSION