Tipos de retorno assíncronos (C#)

Métodos assíncronos podem conter os seguintes tipos de retorno:

  • Task, para um método assíncrono que executa uma operação, mas não retorna nenhum valor.
  • Task<TResult>, para um método assíncrono que retorna um valor.
  • void, para um manipulador de eventos.
  • Qualquer tipo que tenha um método GetAwaiter acessível. O objeto retornado pelo método GetAwaiter deve implementar a interface System.Runtime.CompilerServices.ICriticalNotifyCompletion.
  • IAsyncEnumerable<T>, para um método assíncrono que retorna um fluxo assíncrono.

Para obter mais informações sobre os métodos assíncronos, confira Programação assíncrona com async e await (C#).

Também existem vários outros tipos específicos para cargas de trabalho do Windows:

Tipo de retorno da tarefa

Os métodos assíncronos que não contêm uma instrução return ou que contêm uma instrução return que não retorna um operando, normalmente têm um tipo de retorno de Task. Esses métodos retornam void se eles são executados de forma síncrona. Se você usar um tipo de retorno Task para um método assíncrono, um método de chamada poderá usar um operador await para suspender a conclusão do chamador até que o método assíncrono chamado seja concluído.

No exemplo a seguir, o método WaitAndApologizeAsync não contém uma instrução return, então o método retorna um objeto Task. Retornar Task habilita a espera por WaitAndApologizeAsync. O tipo Task não inclui uma propriedade Result porque ele não tem nenhum valor retornado.

public static async Task DisplayCurrentInfoAsync()
{
    await WaitAndApologizeAsync();

    Console.WriteLine($"Today is {DateTime.Now:D}");
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Console.WriteLine("The current temperature is 76 degrees.");
}

static async Task WaitAndApologizeAsync()
{
    await Task.Delay(2000);

    Console.WriteLine("Sorry for the delay...\n");
}
// Example output:
//    Sorry for the delay...
//
// Today is Monday, August 17, 2020
// The current time is 12:59:24.2183304
// The current temperature is 76 degrees.

O WaitAndApologizeAsync é aguardado, usando uma instrução await, em vez de uma expressão await, semelhante à instrução de chamada a um método síncrono de retorno void. A aplicação de um operador await, nesse caso, não produz um valor. Quando o operando direito de await é Task<TResult>, a expressão await produz o resultado T. Quando o operando direito de await é Task, await e o operando dele são uma instrução.

Você pode separar a chamada a WaitAndApologizeAsync da aplicação de um operador await, como mostrado no código a seguir. No entanto, lembre-se que uma Task não tem uma propriedade Result e que nenhum valor será produzido quando um operador await for aplicado a uma Task.

O código a seguir separa a chamada ao método WaitAndApologizeAsync da espera pela tarefa que o método retorna.

Task waitAndApologizeTask = WaitAndApologizeAsync();

string output =
    $"Today is {DateTime.Now:D}\n" +
    $"The current time is {DateTime.Now.TimeOfDay:t}\n" +
    "The current temperature is 76 degrees.\n";

await waitAndApologizeTask;
Console.WriteLine(output);

Tipo de retorno Task<TResult>

O tipo de retorno Task<TResult> é usado para um método assíncrono que contém uma instrução return em que o operando é TResult.

No exemplo a seguir, o método GetLeisureHoursAsync contém uma instrução return que retorna um número inteiro. A declaração do método deve especificar um tipo de retorno igual a Task<int>. O método assíncrono FromResult é um espaço reservado para uma operação que retorna DayOfWeek.

public static async Task ShowTodaysInfoAsync()
{
    string message =
        $"Today is {DateTime.Today:D}\n" +
        "Today's hours of leisure: " +
        $"{await GetLeisureHoursAsync()}";

    Console.WriteLine(message);
}

static async Task<int> GetLeisureHoursAsync()
{
    DayOfWeek today = await Task.FromResult(DateTime.Now.DayOfWeek);

    int leisureHours =
        today is DayOfWeek.Saturday || today is DayOfWeek.Sunday
        ? 16 : 5;

    return leisureHours;
}
// Example output:
//    Today is Wednesday, May 24, 2017
//    Today's hours of leisure: 5

Quando GetLeisureHoursAsync é chamado de dentro de uma expressão await no método ShowTodaysInfo, a expressão await recupera o valor inteiro (o valor de leisureHours) que está armazenado na tarefa que é retornada pelo método GetLeisureHours. Para obter mais informações sobre expressões await, consulte await.

Você pode entender melhor como await recupera o resultado de Task<T> separando a chamada a GetLeisureHoursAsync da aplicação de await, como mostra o código a seguir. Uma chamada ao método GetLeisureHoursAsync que não é aguardada imediatamente, retorna um Task<int>, como você esperaria da declaração do método. A tarefa é atribuída à variável getLeisureHoursTask no exemplo. Já que getLeisureHoursTask é um Task<TResult>, ele contém uma propriedade Result do tipo TResult. Nesse caso, TResult representa um tipo inteiro. Quando await é aplicado à getLeisureHoursTask, a expressão await é avaliada como o conteúdo da propriedade Result de getLeisureHoursTask. O valor é atribuído à variável ret.

Importante

A propriedade Result é uma propriedade de bloqueio. Se você tentar acessá-la antes que sua tarefa seja concluída, o thread que está ativo no momento será bloqueado até que a tarefa seja concluída e o valor esteja disponível. Na maioria dos casos, você deve acessar o valor usando await em vez de acessar a propriedade diretamente.

O exemplo anterior recuperou o valor da propriedade Result para bloquear o thread principal, de modo que o método Main pudesse imprimir message no console antes do encerramento do aplicativo.

var getLeisureHoursTask = GetLeisureHoursAsync();

string message =
    $"Today is {DateTime.Today:D}\n" +
    "Today's hours of leisure: " +
    $"{await getLeisureHoursTask}";

Console.WriteLine(message);

Tipo de retorno nulo

O tipo de retorno void é usado em manipuladores de eventos assíncronos, que exigem um tipo de retorno void. Para métodos diferentes de manipuladores de eventos que não retornam um valor, você deve retornar um Task, porque não é possível esperar por um método assíncrono que retorna void. Qualquer chamador desse método deve continuar a conclusão sem esperar que o método assíncrono chamado seja concluído. O chamador deve ser independente de quaisquer valores ou exceções que o método assíncrono gere.

O chamador de um método assíncrono de retorno nulo não pode capturar exceções geradas por meio do método. Essas exceções sem tratamento provavelmente causarão falha no aplicativo. Se um método que retorna Task ou Task<TResult> gera uma exceção, ela é armazenada na tarefa retornada. A exceção é gerada novamente quando a tarefa é aguardada. Verifique se qualquer método assíncrono que pode produzir uma exceção tem o tipo de retorno Task ou Task<TResult> e se as chamadas ao método são aguardadas.

O exemplo a seguir mostra o comportamento de um manipulador de eventos assíncrono. No exemplo de código, um manipulador de eventos assíncrono deve informar o thread principal quando for concluído. Em seguida, o thread principal pode aguardar um manipulador de eventos assíncronos ser concluído antes de sair do programa.

public class NaiveButton
{
    public event EventHandler? Clicked;

    public void Click()
    {
        Console.WriteLine("Somebody has clicked a button. Let's raise the event...");
        Clicked?.Invoke(this, EventArgs.Empty);
        Console.WriteLine("All listeners are notified.");
    }
}

public class AsyncVoidExample
{
    static readonly TaskCompletionSource<bool> s_tcs = new TaskCompletionSource<bool>();

    public static async Task MultipleEventHandlersAsync()
    {
        Task<bool> secondHandlerFinished = s_tcs.Task;

        var button = new NaiveButton();

        button.Clicked += OnButtonClicked1;
        button.Clicked += OnButtonClicked2Async;
        button.Clicked += OnButtonClicked3;

        Console.WriteLine("Before button.Click() is called...");
        button.Click();
        Console.WriteLine("After button.Click() is called...");

        await secondHandlerFinished;
    }

    private static void OnButtonClicked1(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 1 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 1 is done.");
    }

    private static async void OnButtonClicked2Async(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 2 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 2 is about to go async...");
        await Task.Delay(500);
        Console.WriteLine("   Handler 2 is done.");
        s_tcs.SetResult(true);
    }

    private static void OnButtonClicked3(object? sender, EventArgs e)
    {
        Console.WriteLine("   Handler 3 is starting...");
        Task.Delay(100).Wait();
        Console.WriteLine("   Handler 3 is done.");
    }
}
// Example output:
//
// Before button.Click() is called...
// Somebody has clicked a button. Let's raise the event...
//    Handler 1 is starting...
//    Handler 1 is done.
//    Handler 2 is starting...
//    Handler 2 is about to go async...
//    Handler 3 is starting...
//    Handler 3 is done.
// All listeners are notified.
// After button.Click() is called...
//    Handler 2 is done.

Tipos de retorno assíncronos generalizados e ValueTask<TResult>

Um método assíncrono pode retornar qualquer tipo que tenha um método GetAwaiter acessível que retorne uma instância de um tipo de aguardador. Além disso, o tipo retornado do método GetAwaiter deve ter o atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Saiba mais no artigo sobre Atributos lidos pelo compilador ou na especificação de C# para o Padrão de construtor de tipo de tarefa.

Esse recurso é o complemento para expressões aguardáveis, que descreve os requisitos para o operando await. Tipos de retorno assíncronos generalizados permitem que o compilador gere métodos async que retornam tipos diferentes. Tipos de retorno assíncronos generalizados habilitaram melhorias de desempenho nas bibliotecas .NET. Já que Task e Task<TResult> são tipos de referência, a alocação de memória em caminhos críticos para o desempenho, especialmente quando alocações ocorrerem em loops estreitos, podem afetar o desempenho. Suporte para tipos de retorno generalizados significa que você pode retornar um tipo de valor leve em vez de um tipo de referência para evitar as alocações de memória adicionais.

O .NET fornece a estrutura System.Threading.Tasks.ValueTask<TResult> como uma implementação leve de um valor de retorno de tarefa generalizado. O exemplo a seguir usa a estrutura ValueTask<TResult> para recuperar o valor de dois lançamentos de dados.

class Program
{
    static readonly Random s_rnd = new Random();

    static async Task Main() =>
        Console.WriteLine($"You rolled {await GetDiceRollAsync()}");

    static async ValueTask<int> GetDiceRollAsync()
    {
        Console.WriteLine("Shaking dice...");

        int roll1 = await RollAsync();
        int roll2 = await RollAsync();

        return roll1 + roll2;
    }

    static async ValueTask<int> RollAsync()
    {
        await Task.Delay(500);

        int diceRoll = s_rnd.Next(1, 7);
        return diceRoll;
    }
}
// Example output:
//    Shaking dice...
//    You rolled 8

Escrever um tipo de retorno assíncrono generalizado é um cenário avançado, direcionado para uso em ambientes especializados. Considere o uso dos tipos Task, Task<T> e ValueTask<T>, que abrangem a maioria dos cenários para código assíncrono.

No C# 10 e versões posteriores, você pode aplicar o atributo AsyncMethodBuilder a um método assíncrono (em vez da declaração de tipo de retorno assíncrono) para substituir o construtor daquele tipo. Normalmente, você aplicaria esse atributo para usar um construtor diferente fornecido no runtime do .NET.

Fluxos assíncronos com IAsyncEnumerable<T>

Um método assíncrono pode retornar um fluxo assíncrono, representado por IAsyncEnumerable<T>. Um fluxo assíncrono fornece uma forma de enumerar itens lidos de um fluxo quando os elementos são gerados em partes com chamadas assíncronas repetidas. O seguinte exemplo mostra um método assíncrono que gera um fluxo assíncrono:

static async IAsyncEnumerable<string> ReadWordsFromStreamAsync()
{
    string data =
        @"This is a line of text.
              Here is the second line of text.
              And there is one more for good measure.
              Wait, that was the penultimate line.";

    using var readStream = new StringReader(data);

    string? line = await readStream.ReadLineAsync();
    while (line != null)
    {
        foreach (string word in line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
        {
            yield return word;
        }

        line = await readStream.ReadLineAsync();
    }
}

O exemplo anterior lê linhas de uma cadeia de caracteres de modo assíncrono. Depois que cada linha é lida, o código enumera cada palavra na cadeia de caracteres. Os chamadores enumerariam cada palavra usando a instrução await foreach. O método aguarda quando precisa ler de modo assíncrono a próxima linha da cadeia de caracteres de origem.

Confira também