ValueTask<T>
não é um subconjunto Task<T>
, é um superconjunto .
ValueTask<T>
é uma união discriminada de um T e um Task<T>
, tornando-o livre de alocação para ReadAsync<T>
retornar de forma síncrona um valor T disponível (em contraste com o uso Task.FromResult<T>
, que precisa alocar uma Task<T>
instância). ValueTask<T>
é aguardável, portanto, a maior parte do consumo de instâncias será indistinguível de com a Task<T>
.
O ValueTask, por ser uma estrutura, permite escrever métodos assíncronos que não alocam memória quando executados de forma síncrona sem comprometer a consistência da API. Imagine ter uma interface com um método de retorno de tarefas. Cada classe que implementa essa interface deve retornar uma tarefa, mesmo que seja executada de forma síncrona (usando o Task.FromResult). É claro que você pode ter 2 métodos diferentes na interface, um síncrono e um assíncrono, mas isso requer 2 implementações diferentes para evitar “sincronização por async” e “async por sincronização”.
Assim, você pode escrever um método assíncrono ou síncrono, em vez de escrever um método idêntico para cada um. Você pode usá-lo em qualquer lugar que usar, Task<T>
mas muitas vezes não adicionaria nada.
Bem, isso acrescenta uma coisa: adiciona uma promessa implícita ao chamador de que o método realmente usa a funcionalidade adicional que ValueTask<T>
fornece. Pessoalmente, prefiro escolher os tipos de parâmetro e retorno que informam ao chamador o máximo possível. Não retorne IList<T>
se a enumeração não puder fornecer uma contagem; não volte IEnumerable<T>
se puder. Seus consumidores não precisam procurar nenhuma documentação para saber quais dos seus métodos podem ser razoavelmente chamados de forma síncrona e quais não.
Não vejo mudanças no design futuro como um argumento convincente lá. Muito pelo contrário: se um método alterar sua semântica, ele deve interromper a construção até que todas as chamadas sejam atualizadas de acordo. Se isso for considerado indesejável (e acredite, sou solidário com o desejo de não quebrar a compilação), considere o versionamento da interface.
É para isso que serve a digitação forte.
Se alguns dos programadores que projetam métodos assíncronos em sua loja não puderem tomar decisões informadas, pode ser útil designar um mentor sênior para cada um desses programadores menos experientes e fazer uma revisão semanal do código. Se eles acharem errado, explique por que isso deve ser feito de maneira diferente. É uma sobrecarga para os caras mais velhos, mas isso levará os juniores a acelerar muito mais rapidamente do que apenas jogá-los no fundo do poço e dar a eles alguma regra arbitrária a seguir.
Se o cara que escreveu o método não sabe se pode ser chamado de forma síncrona, quem sabe ?!
Se você tem tantos programadores inexperientes escrevendo métodos assíncronos, essas mesmas pessoas também os chamam? Eles estão qualificados para descobrir por si mesmos quais são seguros para chamar de assíncrono ou começarão a aplicar uma regra igualmente arbitrária à maneira como chamam essas coisas?
O problema aqui não são seus tipos de retorno, são os programadores sendo colocados em funções para as quais não estão prontos. Isso deve ter acontecido por um motivo, por isso tenho certeza de que não pode ser trivial de corrigir. Descrevê-lo certamente não é uma solução. Mas procurar uma maneira de ocultar o problema além do compilador também não é uma solução.
ValueTask<T>
(em termos de alocações) não se materializar para operações que são realmente assíncronas (porque nesse casoValueTask<T>
ainda precisará de alocação de heap). Há também a questão deTask<T>
ter muito outro suporte nas bibliotecas.