C# - Novos tipos de retornos para Async


Hoje veremos os novos tipos de retornos que podem ser usados para os métodos assíncronos usando async.

Você já deve conhecer os operadores async/await usados na programação assíncrona na linguagem C#.

Antes do C# 7.0 , todo método assíncrono tinha que retornar Task, Task<T> ou void :

Pois bem a partir do C# 7.0 os tipos de retorno foram estendidos e agora podemos retornar o tipo ValueTask<T>.

Devemos lembrar que Task é uma classe, que é um tipo por referência, e que em algumas situações é melhor retornar outro valor ao invés de uma Task.

Por isso agora podemos retornar um tipo mais leve no lugar de retornar um tipo de referência  para evitar alocações de memória adicionais, e, a partir do C# 7, existe um tipo de valor embutido ValueTask<T> que pode ser usado em vez da Task<T>.

Agora a plataforma  .NET fornece o System.Threading.Tasks.ValueTask<TResult> como uma implementação leve de um valor generalizado de retorno de Task. 

Para usar o tipo System.Threading.Tasks.ValueTask<TResult>, você deve adicionar o pacote NuGet System.Threading.Tasks.Extensions ao seu projeto.

Exemplo:

        static async Task Main(string[] args)
        {
            await MetodoSemRetornoDeValor();
            Console.WriteLine("A tarefa foi concluida");
            var result2 = await MetodoRetornandoValor();
            Console.WriteLine($"Valor retornado {result2}");
            Console.ReadKey();
        }
        static async ValueTask MetodoSemRetornoDeValor()
        {
            Console.WriteLine("MetodoSemRetornoDeValor");
            await Task.Delay(2000);
        }

        static async ValueTask<int> MetodoRetornandoValor()
        {
            Console.WriteLine("MetodoRetornandoValor");
            await Task.Delay(2000);
            return 2020;
        }

Temos um método assíncrono usando ValueTask o que indica que ele vai fornecer um resultado esperado que é a execução da tarefa

Aqui Task.Delay(2000) vai criar uma tarefa que será completada após o tempo definido de 2000 milisegundos ou 2 segundos ser decorrido.

Esta tarefa será executada em um segmento diferente, e a interface de usuário (IU) vai estar responsiva.

Esse comportamento ocorre porque Task.Delay() não está bloqueando a thread principal.

A seguir temos o método assíncrono que retorna um int após a conclusão da tarefa.

Existe uma grande grande diferença entre a Task e  ValueTask.

Um Task é um tipo de referência e requer alocação de heap. O ValueTask é um tipo de valor e é retornado por valor - ou seja, sem alocação de heap. É recomendável usar o ValueTask quando houver uma alta probabilidade de que um método não precise esperar por operações assíncronas.

Por exemplo, se um método retornar resultados em cache ou resultados predefinidos,  isso pode reduzir significativamente o número de alocações e resultar em grande melhoria de desempenho.

   class Exemplo2
    {
        public async ValueTask<string> GetNomeAsync(int id)
        {
            if (TentaPegarNomeDoCache(id, out var nome))
            {
                return nome;
            }
            nome = await GetNomeBancoDados(id);
            AdicionaNoCache(id, nome);
            return nome;
        }
    }

Mas existe uma desvantagem do uso do ValueTask : ele não é totalmente compatível com o Task.

Se houver um método retornando Task e chamar um método retornando ValueTask, não há como converter implicitamente ValueTask em Task.

    class Program
    {
        static Task<int> GetValorComTask() => Task.FromResult(1);
        static ValueTask<int> GetValorComValueTask() => new ValueTask<int>(1);
        static async Task Main(string[] args)
        {
            var result1 = await GetValor();
            var result2 = await GetValorAsync();
            Console.WriteLine($"GetValor = {result1}");
            Console.WriteLine($"GetValorAsync = {result2}");
            Console.ReadKey();
        }
        static Task<int> GetValor()
        {
            // Não Compila
            // CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task<int>' 
            // to 'System.Threading.Tasks.ValueTask<int>'
            // return GetValorComTask();
            return GetValorComValueTask();
        }
        static async ValueTask<int> GetValorAsync()
        {
            // funciona sem erros
            return await GetValorComTask();
        }
    }

O que podemos fazer é usar o método AsTask que recupera um objeto Task<T> que representa este ValueTask<T>.

No exemplo acima podemos alterar o código do método GetValor usando AsTask:

   static Task<int> GetValor()
   {
            var result = GetValorComValueTask();

            return result.AsTask();
   }

Agora o código vai compilar; mas porque você faria isso visto que agindo assim não há vantagem alguma em usar ValueTask.

Assim devemos usar ValueTask com cautela em cenários onde o método vai operar de forma síncrona na maioria das chamadas e de forma assíncrona eventualmente.

E estamos conversados.

Veja mais detalhes na documentação oficial.

"Não se turbe o vosso coração; credes em Deus, crede também em mim.
Na casa de meu Pai há muitas moradas; se não fosse assim, eu vo-lo teria dito. Vou preparar-vos lugar."

João 14:1,2

Referências:


José Carlos Macoratti