1. Introdução
Antes de cair no código, vamos entender de forma simples. Lembre-se que criar um objeto no .NET (e ainda mais — uma task!) custa memória e um pouco de tempo de CPU. Agora imagine uma API assíncrona que na metade das vezes retorna o resultado imediatamente (por exemplo, pega do cache) e na outra metade consulta o banco de dados, então a operação fica realmente assíncrona e precisa de um Task. Isso aparece com frequência — por exemplo, em caches de arquivo, APIs de rede, pools de objetos e outros cenários "assíncronos, mas às vezes instantâneos".
Se nós sempre retornarmos Task, mesmo para resultados instantâneos teremos que criar objetos desnecessários. E se pudéssemos retornar o resultado sem uma task quando ele já estiver pronto? Foi para isso que surgiu o ValueTask.
Fato: os padrões Task.CompletedTask e Task.FromResult(…) realmente economizam tempo de execução, mas criam um objeto compartilhado que pode não ser ideal em cenários de alta carga.
O que é ValueTask
ValueTask é uma struct-wrapper especial que pode representar ou um resultado já pronto, ou (se a operação for realmente assíncrona) a própria task. Em outras palavras, é um "pacote" que pode conter ou simplesmente o valor, ou uma referência para um Task.
Existem duas variantes principais:
- ValueTask — "sem valor" (não retorna valor, como Task)
- ValueTask<TResult> — wrapper para um valor (análogo a Task<TResult>)
Comparando Task e ValueTask
| Tipo | Número de alocações | Pode ser concluído sincronicamente | Usado normalmente |
|---|---|---|---|
|
Uma (heap) | Sim/Não | Quase sempre |
|
Nenhuma/uma | Sim/Não | Otimização |
|
Nenhuma/uma | Sim/Não | Otimização |
Quando aplicar ValueTask
Existe uma regra de ouro: se você escreve um método assíncrono comum que sempre retorna o resultado apenas depois de uma operação async, use Task. É simples, seguro e compreensível para todo mundo.
Use ValueTask quando:
- O resultado pode ser obtido sincronicamente (por exemplo, do cache, pool, memória), e você quer economizar alocações desnecessárias.
- A operação assíncrona acontece não com tanta frequência (caso contrário os benefícios se perdem frente à complexidade do código e cópia de structs).
Atenção! Se você sempre retorna um resultado assíncrono — use Task. Se você quer que sua API seja "super-otimizada" para resultados instantâneos frequentes — então ValueTask.
2. Resultado sincrono ou assíncrono
Vamos ver uma função que procura um usuário por nome. Se o usuário estiver no cache — retornamos instantaneamente; se não — carregamos assincronamente do "banco":
// Modelamos nosso usuário
public class User
{
public string Name { get; set; }
}
// Nosso cache (muito simples)
private readonly Dictionary<string, User> _localCache = new();
public async ValueTask<User> FindUserAsync(string name)
{
// Verificamos o cache local
if (_localCache.TryGetValue(name, out var user))
{
// Resultado instantâneo — nenhuma alocação de task!
return user;
}
// Aqui uma suposta operação assíncrona longa (por exemplo, do banco de dados)
user = await LoadUserFromDbAsync(name);
// Colocamos no cache para o futuro
_localCache[name] = user;
return user;
}
private async Task<User> LoadUserFromDbAsync(string name)
{
// Simulamos atraso
await Task.Delay(500);
return new User { Name = name };
}
Importante: No caso de cache-hit retornamos o resultado diretamente — sem alocação de task! Só quando o resultado precisa ser "buscado" realmente criamos a tarefa assíncrona.
Como ValueTask é por dentro
Dentro do ValueTask pode estar ou um valor pronto, ou uma referência para um Task:
ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));
Ao usar await o compilador resolve automaticamente: se o resultado for instantâneo, não serão criados objetos extras.
Importante sobre await e ValueTask
Métodos async se dão bem com await para ValueTask:
public async ValueTask PingAsync()
{
// ...
await Task.Delay(10);
}
Mas se você guardar uma instância de ValueTask e decidir aguardá-la mais tarde, lembre-se: não é permitido fazer await da mesma instância mais de uma vez. Task pode ser aguardada múltiplas vezes, ValueTask não.
Erro: await repetido em ValueTask
ValueTask<int> task = ComputeAsync();
// Isso é OK
int a = await task;
// Isso é erro! Segundo await na mesma instância de ValueTask não é permitido:
// int b = await task; // NÃO PODE!
3. Reescrevendo parte do nosso aplicativo de console
Suponha que estamos desenvolvendo um mini-leitor de livros: parte dos textos já está carregada no cache, o resto é carregado assincronamente. Vamos implementar um método otimizado para obter a primeira linha do livro 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;
}
// Suponha que haja um download assíncrono do livro
var downloadedBook = await DownloadBookTextAsync(title);
_bookCache[title] = downloadedBook;
return downloadedBook.Split('\n')[0];
}
private async Task<string> DownloadBookTextAsync(string title)
{
// Simulamos atraso (por exemplo, download da internet)
await Task.Delay(1000);
return $"Book: {title}\nThis is the first line.\nSecond line...";
}
É assim que ValueTask economiza a criação de uma task quando o livro já está no cache.
4. Nuances úteis
Marcador: quando não usar ValueTask
Se sua API é sempre assíncrona (por exemplo, sempre comunica com a rede) — use Task, é mais simples e seguro.
Como transformar ValueTask em Task e vice-versa
Às vezes ValueTask "não cabe" numa API que espera Task. Use .AsTask():
ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();
Se precisar transformar um Task em ValueTask — crie um ValueTask a partir da task:
Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);
Comparação: ValueTask vs Task
| Característica | |
|
|---|---|---|
| Await repetido | Permitido | Não permitido |
| Alocações em respostas rápidas | Existe | Não (se o resultado for instantâneo) |
| Compatibilidade de interface | Ampla suporte | Requer wrappers adicionais |
| Aplicabilidade | Em todo lugar | Apenas para otimização |
| Pooling (Pool) | Usa pool comum | Não, é struct |
| Simplicidade | Simples | Mais complexo |
Uso de ValueTask na vida real
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;
}
Aplicação em entrevista: Se te perguntarem como melhorar ainda mais a performance de uma API assíncrona com cache — mencione o ValueTask.
Exemplos de compatibilidade com LINQ e IAsyncEnumerable<T>
Se você quiser usar ValueTask com LINQ assíncrono (IAsyncEnumerable<T>), isso é suportado no .NET (por exemplo, o método ToListAsync):
public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
// Simulamos que parte dos números já está calculada, e parte é operação assíncrona
// ... (exemplo omitido por brevidade)
return new List<int> { 2, 3, 5, 7, 11 };
}
5. Conclusões e características de implementação
- Use ValueTask para otimizar quando uma parte significativa do tempo seu resultado está pronta instantaneamente (por exemplo, do cache).
- Não use ValueTask "só por usar" — isso vai complicar o código e causar erros.
- Não await um ValueTask várias vezes. Se precisar, converta para Task.
- Toda a compatibilidade com Task é via .AsTask().
- Lembre que ValueTask é um struct, e pode se comportar diferente quando copiada.
6. Erros típicos ao trabalhar com ValueTask
Erro nº1: await repetido na mesma instância de ValueTask. ValueTask é uma struct, não se deve await-á-la duas vezes. O segundo await vai lançar exceção ou funcionar de forma incorreta.
Erro nº2: usar ValueTask sem checar compatibilidade. Alguns APIs externas exigem Task, e passar ValueTask diretamente pode causar problemas.
Erro nº3: usar ValueTask sem necessidade. Se o resultado é sempre assíncrono, usar ValueTask complica o código sem benefício real.
Erro nº4: copiar ValueTask como struct. Uma struct copiada pode se comportar de forma inesperada ao await; é importante trabalhar com o original.
GO TO FULL VERSION