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 acessível GetAwaiter . O objeto retornado pelo GetAwaiter método deve implementar a System.Runtime.CompilerServices.ICriticalNotifyCompletion interface.
  • IAsyncEnumerable<T>, para um método assíncrono que retorna um fluxo assíncrono.

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

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

Tipo de retorno de tarefa

Os métodos assíncronos que não contêm uma return instrução ou que contêm uma return instrução 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 Task tipo de retorno para um método assíncrono, um método de chamada poderá usar um await operador para suspender a conclusão do chamador até que o método assíncrono chamado seja concluído.

No exemplo a seguir, o WaitAndApologizeAsync método não contém uma return instrução, portanto, o método retorna um Task objeto. A devolução de um Task permite WaitAndApologizeAsync ser aguardado. O Task tipo não inclui uma Result propriedade porque não tem valor de retorno.

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.

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

Você pode separar a chamada para WaitAndApologizeAsync do aplicativo de um operador await, como mostra o código a seguir. No entanto, lembre-se de que um Task não tem uma Result propriedade e que nenhum valor é produzido quando um operador de espera é aplicado a um Task.

O código a seguir separa chamar o WaitAndApologizeAsync método de aguardar a 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 Task<TResult> tipo de retorno é usado para um método assíncrono que contém uma instrução return na qual o operando é TResult.

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

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 é chamada de dentro de uma expressão await no ShowTodaysInfo método, a expressão await recupera o valor inteiro (o valor de ) armazenado na tarefa retornada leisureHourspelo GetLeisureHours método. Para obter mais informações sobre expressões await, consulte await.

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

Importante

A Result propriedade é uma propriedade de bloqueio. Se você tentar acessá-lo 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 Result valor da propriedade para bloquear o thread principal para que o Main método pudesse imprimir o message no console antes que o aplicativo terminasse.

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 void tipo de retorno em manipuladores de eventos assíncronos, que exigem um tipo de void retorno. Para métodos diferentes de 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 de tal método deve continuar a conclusão sem esperar que o método assíncrono chamado termine. O chamador deve ser independente de quaisquer valores ou exceções que o método assíncrono gera.

O chamador de um método assíncrono de retorno de vazio não pode capturar exceções lançadas do método. Essas exceções não tratadas provavelmente farão com que seu aplicativo falhe. Se um método que retorna uma Task ou Task<TResult> lança uma exceção, a exceção é armazenada na tarefa retornada. A exceção é relançada quando a tarefa é aguardada. Certifique-se de que qualquer método assíncrono que possa produzir uma exceção tenha um tipo de retorno de Task ou Task<TResult> e que as chamadas para o método sejam 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 terminar. Em seguida, o thread principal pode aguardar a conclusão de um manipulador de eventos assíncrono 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íncrono generalizado e ValueTask<TResult>

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

Este recurso é o complemento para expressões aguardadas, que descreve os requisitos para o operando de await. Os tipos de retorno assíncronos generalizados permitem que o compilador gere async métodos que retornam tipos diferentes. Os tipos de retorno assíncrono generalizado habilitaram melhorias de desempenho nas bibliotecas .NET. Como Task e Task<TResult> são tipos de referência, a alocação de memória em caminhos críticos de desempenho, particularmente quando as alocações ocorrem 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 alocações de memória adicionais.

O .NET fornece a System.Threading.Tasks.ValueTask<TResult> estrutura como uma implementação leve de um valor generalizado de retorno de tarefas. O exemplo a seguir usa a ValueTask<TResult> estrutura 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. Em vez disso, considere usar os tipos , Task<T>e ValueTask<T> , que abrangem a maioria dos cenários para código assíncronoTask.

Em C# 10 e posterior, você pode aplicar o atributo a um método assíncrono AsyncMethodBuilder (em vez da declaração de tipo de retorno assíncrono) para substituir o construtor desse tipo. Normalmente, você aplicaria esse atributo para usar um construtor diferente fornecido no tempo de execução 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 await foreach instrução. O método aguarda quando precisa ler assincronamente a próxima linha da cadeia de caracteres de origem.

Consulte também