1. Introduzione
Prima di tuffarci nel codice, facciamolo semplice. Ricordate che creare un oggetto in .NET (e ancor di più — un task!) costa memoria e un po' di tempo CPU? Ora immaginate un'API asincrona che nel 50% dei casi restituisce il risultato istantaneamente (per esempio lo prende dalla cache), e nell'altro 50% consulta il database, quindi l'operazione diventa realmente asincrona e richiede un Task. Succede spesso — per esempio in cache di file, API di rete, pool di oggetti e altri scenari "asincroni, ma a volte istantanei".
Se restituiamo sempre un Task, anche per risultati istantanei dobbiamo creare oggetti inutili. E se potessimo restituire il risultato senza il task quando è già pronto? Proprio per questo è nato ValueTask.
Fatto: i soliti Task.CompletedTask e Task.FromResult(…) risparmiano tempo di esecuzione, ma creano oggetti condivisi che potrebbero non essere ideali per scenari ad alto carico.
Cos'è ValueTask
ValueTask è una struct-wrapper speciale che può rappresentare o un risultato già pronto, o (se l'operazione è veramente asincrona) il task stesso. In parole povere, è un "pacchetto" che può contenere o solo il valore oppure un riferimento a un Task.
Ci sono due varianti principali:
- ValueTask — "senza valore" (non restituisce un valore, come Task)
- ValueTask<TResult> — wrapper per un valore (analogo a Task<TResult>)
Confronto tra Task e ValueTask
| Tipo | Numero di allocazioni | Può essere completato sincronicamente | Usi tipici |
|---|---|---|---|
|
Una (heap) | Sì/No | Quasi sempre |
|
Zero/uno | Sì/No | Ottimizzazione |
|
Zero/uno | Sì/No | Ottimizzazione |
Quando usare ValueTask
C'è una regola d'oro: se scrivi un metodo asincrono che restituisce sempre il risultato solo dopo un'operazione async, usa Task. È semplice, sicuro e comprensibile a tutti.
Usa ValueTask quando:
- Il risultato può essere ottenuto sincronicamente (per esempio dalla cache, da un pool, dalla memoria), e vuoi risparmiare sulle allocazioni inutili.
- L'operazione asincrona capita non troppo spesso (altrimenti i benefici si perdono a fronte della complessità del codice e della copia delle struct).
Attenzione! Se restituisci sempre un risultato asincrono — usa Task. Se vuoi che la tua API sia "super-ottimale" per risultati istantanei frequenti — allora ValueTask.
2. Risultato sincrono o asincrono
Consideriamo una funzione che cerca un utente per nome. Se l'utente è in cache — lo restituiamo immediatamente; se no — lo carichiamo asincronicamente dal "database":
// Modelliamo il nostro utente
public class User
{
public string Name { get; set; }
}
// La nostra cache (molto semplice)
private readonly Dictionary<string, User> _localCache = new();
public async ValueTask<User> FindUserAsync(string name)
{
// Controlliamo la cache locale
if (_localCache.TryGetValue(name, out var user))
{
// Risultato immediato — nessuna allocazione di task!
return user;
}
// Qui una presunta lunga operazione asincrona (per esempio dal DB)
user = await LoadUserFromDbAsync(name);
// Mettiamo in cache per il futuro
_localCache[name] = user;
return user;
}
private async Task<User> LoadUserFromDbAsync(string name)
{
// Simuliamo una latenza
await Task.Delay(500);
return new User { Name = name };
}
Importante: In caso di hit nella cache restituiamo il risultato normalmente — nessuna allocazione di task! Solo quando dobbiamo "recuperare" il risultato creiamo davvero un'operazione asincrona.
Com'è fatto internamente ValueTask
Dentro a ValueTask può esserci o un valore pronto, o un riferimento a un Task:
ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));
Quando usi await il compilatore capisce da solo: se il risultato è immediato non creerà oggetti extra.
Importante su await e ValueTask
I metodi async vanno d'accordo con await per ValueTask:
public async ValueTask PingAsync()
{
// ...
await Task.Delay(10);
}
Ma se salvi un'istanza di ValueTask e decidi di attenderla più tardi, ricorda: non si può fare await sulla stessa istanza più di una volta. Un Task può essere atteso molte volte, un ValueTask no.
Errore: await ripetuto su ValueTask
ValueTask<int> task = ComputeAsync();
// Questo va bene
int a = await task;
// Questo è un errore! Un secondo await sulla stessa istanza di ValueTask non è permesso:
// int b = await task; // NON SI PUÒ!
3. Riscriviamo parte della nostra applicazione console
Supponiamo di sviluppare un mini-reader di libri: parte dei testi è già caricata nella cache, il resto viene scaricato asincronicamente. Implementiamo un metodo ottimizzato per ottenere la prima riga di un libro usando ValueTask:
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;
}
// Supponiamo il download asincrono del libro
var downloadedBook = await DownloadBookTextAsync(title);
_bookCache[title] = downloadedBook;
return downloadedBook.Split('\n')[0];
}
private async Task<string> DownloadBookTextAsync(string title)
{
// Simuliamo una latenza (per esempio scaricamento da internet)
await Task.Delay(1000);
return $"Book: {title}\nThis is the first line.\nSecond line...";
}
Così ValueTask risparmia la creazione di un task quando il libro è già in cache.
4. Dettagli utili
Segnale: quando NON usare ValueTask
Se la tua API è sempre asincrona (per esempio sempre comunicazione di rete) — usa Task, è più semplice e sicuro.
Come convertire ValueTask in Task e viceversa
Capita che un ValueTask non si adatti a un'API che vuole Task. Usa .AsTask():
ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();
Se devi trasformare un Task in ValueTask — crea semplicemente un ValueTask dal task:
Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);
Confronto: ValueTask vs Task
| Caratteristica | |
|
|---|---|---|
| Await ripetuto | Permesso | Non permesso |
| Allocazioni per risposte veloci | Ci sono | No (se il risultato è immediato) |
| Compatibilità dell'interfaccia | Ampia supportata | Richiede wrapper aggiuntivi |
| Applicabilità | Ovunque | Solo per ottimizzazione |
| Pooling (Pool) | Usa pool comune | No, è struct |
| Semplicità | Semplice | Più complesso |
Uso di ValueTask nella vita reale
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;
}
Da dire in un colloquio: Se ti chiedono come migliorare ulteriormente le prestazioni di un'API asincrona con caching, parla di ValueTask.
Esempi di compatibilità con LINQ e IAsyncEnumerable<T>
Se vuoi usare ValueTask con LINQ asincrono (IAsyncEnumerable<T>), è supportato in .NET (per esempio il metodo ToListAsync):
public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
// Simuliamo che parte dei numeri sia già calcolata, e parte sia risultato di operazioni asincrone
// ... (esempio omesso per brevità)
return new List<int> { 2, 3, 5, 7, 11 };
}
5. Conclusioni e caratteristiche dell'implementazione
- Usa ValueTask per ottimizzare quando una parte significativa delle volte il risultato è pronto immediatamente (per esempio dalla cache).
- Non usare ValueTask "perché sì" — complicherebbe il codice e causerebbe errori.
- Non fare await su un ValueTask più volte. Se serve, convertilo in Task.
- Tutta la compatibilità con Task passa tramite .AsTask().
- Ricorda che ValueTask è una struct, e può comportarsi diversamente quando viene copiato.
6. Errori tipici quando si lavora con ValueTask
Errore №1: await ripetuto su una stessa istanza di ValueTask. ValueTask è una struct, non può essere await-ata due volte. Un secondo await porterà a eccezioni o comportamenti errati.
Errore №2: usare ValueTask senza verificare la compatibilità. Alcune API esterne richiedono Task, e passare un ValueTask direttamente può creare problemi.
Errore №3: usare ValueTask senza motivo. Se il risultato è sempre asincrono, usare ValueTask complica il codice senza beneficio reale.
Errore №4: copia della struct ValueTask. Una struct copiata può comportarsi inaspettatamente con await, è importante lavorare con l'originale.
GO TO FULL VERSION