Esse é o 10º post da série sobre C# 7, e o último sobre C# 7.0 (seguirei depois falando das versões minor, 7.1, etc). Pra acompanhar a série você pode seguir a tag C#7 no blog ou voltar no post que agrega a série.

Tipos de retornos assíncronos generalizados

A partir do C# 7 é possível utilizar, em funções assíncronas, um tipo de retorno diferente de void, Task, e Task<T>. Isso é especialmente importante porque Task é um tipo que é alocado na heap, ou seja, vai ser coletado pelo GC. Isso significa que se você fizer uso muito intenso de métodos assíncronos, mesmo que eles tenham o resultado já disponível (um cache, por exemplo), você vai pagar o custo da alocação, e isso pode impactar seu desempenho, por causa das contantes paradas para coleta de lixo.

Imagine, por exemplo, uma chamada assíncrona em laço, como for, ou while, algo que pode ser bastante comum hoje em dia, que métodos assíncronos estão por todo lado. Dependendo do tamanho do laço, você pode acabar fazendo muitas alocações.

A Microsoft não espera que você crie novos tipos de retorno assíncrono. Essa nova capacidade foi feita basicamente para que eles mesmos usem, ou para empresas que desenvolvem código de infraestrutura. Por isso mesmo essa funcionalidade já é lançada com o ValueTask<T>, que fica no mesmo namespace de Task, em System.Threading.Tasks. Como o nome diz, uma ValueTask é uma task que não vai causar alocação adicional, é um tipo de valor.

Considere o uso de ValueTask sempre que puder. No exemplo abaixo você vê o uso de ValueTask em várias situações interessantes. A função basicamente faz a soma de 1 até um valor x(mas fazendo chamadas assíncronas para tornar o exemplo seja relevante). Primeiro, note que existe um cache do valor, e se a conta já foi feita, a somaJaCalculada é retornada. Nesse caso, como o método SomaDe1A utiliza ValueTask em vez de Task, não pagaremos a alocação extra pra retornar o valor cacheado. Note também que temos um for, e dentro dele chamamos o método SomaAsync. Esse método simplesmente faz a soma e retorna a ValueTask com o resultado, mas imagine que essa computação fosse cara, e de fato demorasse para concluir. Ela pode acontecer centenas de vezes, mas não pagaremos por centenas de alocações, porque não estamos alocando nada na heap.

private int somaJaCalculada = 0;
async ValueTask<int> SomaDe1A(int x)
{
    if (somaJaCalculada > 0)
        return somaJaCalculada;
    var soma = 0;
    for (int i = 1; i < x; i++)
        soma = await SomaAsync(soma, i);
    somaJaCalculada = soma;
    return soma;
}
ValueTask<int> SomaAsync(int a, int b) => new ValueTask<int>(a + b);

Perceba que a ValueTask é usada normalmente onde a Task seria. A exceção são os métodos estáticos de Task, como Task.Delay, que seguem, pelo menos por enquanto, existindo somente em Task, e ausentes em ValueTask. Também não existe uma ValueTask não genérica, somente a ValueTask<T> existe.

Se você estiver usando o .NET Core 2.0, a ValueTask já estará disponível. Se estiver no .NET Framework (até 4.7.1, o último disponível), ou no .NET Standard (até o 2.0, o último do momento), você precisará instalar o pacote Nuget System.Threading.Tasks.Extensions. Ele vai funcionar com qualquer versão do .NET Standard, e com .NET Framework 4.5 ou superior.

Implementando seu próprio tipo de retorno assíncrono

Don’t.

Sério, não faça isso. A não ser que você tenha certeza que sabe o que está fazendo e precise muito.

Se realmente você quiser fazer isso, vai precisar de um tipo que tenha uma propriedade boleana chamada IsCompleted e um método GetAwaiter que retornará um tipo que implemente INotifyCompletion e que tenha, ele também a propriedade IsCompleted, assim como o método GetResult. Veja o ValueTask e o ValueTaskAwaiter para entender como eles funcionam e se inspirar, eles estão no Github, no repo dotnet/corefx. Aqui está o ValueTask, por exemplo.

E já adianto, a chance de tudo dar errado e o fluxo de execução da sua aplicação ficar completamente fora de controle é alto. Evite a todo custo.

Você consegue ler sobre os tipos de retornos assíncronos generalizados nos docs das novidades do C# 7 no Microsoft Docs.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.