Compartilhar via


Tipos de retorno assíncronos (C#)

Os métodos assíncronos podem ter 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

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 geralmente têm um tipo de retorno de Task. Esses métodos retornam void se forem 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, portanto, o método retorna um objeto Task. Retornar Task habilita a espera por WaitAndApologizeAsync. O tipo Task não inclui uma propriedade Result porque 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 à direita de um await é um Task<TResult>, a expressão await produz um resultado de 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 de aguardar a tarefa retornada pelo método.

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 inteiro. A declaração do método deve especificar um tipo de retorno de 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) armazenado na tarefa 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 uma Task<T> separando a chamada para GetLeisureHoursAsync do aplicativo 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. Como 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 atualmente ativo 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 para que o método Main pudesse imprimir o message no console antes do fim 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

Você usa o tipo de retorno void em manipuladores de eventos assíncronos, que exigem um tipo de retorno void. Para métodos que não sejam manipuladores de eventos que não retornam um valor, você deve retornar um Task em vez disso, porque um método assíncrono que retorna void não pode ser aguardado. 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 um Task ou Task<TResult> gera uma exceção, a exceção é armazenada na tarefa retornada. A exceção é gerada novamente quando a tarefa é aguardada. Verifique se qualquer método assíncrono que possa produzir uma exceção tem um tipo de retorno de Task ou Task<TResult> e se as chamadas para o método são aguardadas.

O exemplo a seguir mostra o comportamento de um manipulador de eventos assíncrono. No código de exemplo, um manipulador de eventos assíncrono deve informar o thread principal quando ele 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 do .NET. Como Task e Task<TResult> são tipos de referência, a alocação de memória em caminhos críticos de desempenho, especialmente quando ocorrem alocações em loops apertados, pode afetar negativamente o desempenho. O 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 mais alocações de memória.

O .NET fornece a estrutura System.Threading.Tasks.ValueTask<TResult> como uma implementação leve de um valor generalizado de retorno de tarefa. O exemplo a seguir usa a estrutura ValueTask<TResult> para recuperar o valor de dois rolos 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 e é direcionado para uso em ambientes especializados. Considere usar os tipos Task, Task<T>e ValueTask<T>, que abrangem a maioria dos cenários de código assíncrono.

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 maneira de enumerar itens lidos de um fluxo quando os elementos são gerados em partes com chamadas assíncronas repetidas. O exemplo a seguir 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 forma assíncrona. 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 assíncronamente a próxima linha da cadeia de caracteres de origem.

Consulte também