Tipos de valor devueltos asincrónicos (C#)

Los métodos asincrónicos pueden tener los siguientes tipos de valor devuelto:

  • Task, para un método asincrónico que realiza una operación pero no devuelve ningún valor.
  • Task<TResult>, para un método asincrónico que devuelve un valor.
  • void, para un controlador de eventos.
  • Cualquier tipo que tenga un método GetAwaiter accesible. El objeto devuelto por el método GetAwaiter debe implementar la interfaz System.Runtime.CompilerServices.ICriticalNotifyCompletion.
  • IAsyncEnumerable<T>, para un método asincrónico que devuelve una secuencia asincrónica.

Para obtener más información sobre los métodos asincrónicos, vea Programación asincrónica con async y await (C#).

También existen varios tipos que son específicos de las cargas de trabajo de Windows:

Tipo de valor devuelto Task

Los métodos asincrónicos que no contienen una instrucción return o que contienen una instrucción return que no devuelve un operando tienen normalmente un tipo de valor devuelto de Task. Dichos métodos devuelven void si se ejecutan de manera sincrónica. Si se usa un tipo de valor devuelto Task para un método asincrónico, un método de llamada puede usar un operador await para suspender la finalización del llamador hasta que finalice el método asincrónico llamado.

En el ejemplo siguiente, el método WaitAndApologizeAsync no contiene una instrucción return, de manera que el método devuelve un objeto Task. La devolución de Task permite que se espere a WaitAndApologizeAsync. El tipo Task no incluye una propiedad Result porque no tiene ningún valor devuelto.

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.

Se espera a WaitAndApologizeAsync mediante una instrucción await en lugar de una expresión await, similar a la instrucción de llamada para un método sincrónico que devuelve void. En este caso, la aplicación de un operador await no genera un valor. Cuando el operando derecho de await es Task<TResult>, la expresión await genera un resultado de T. Cuando el operando derecho de await es Task, await y su operando son una instrucción.

Puede separar la llamada a WaitAndApologizeAsync desde la aplicación de un operador await, como muestra el código siguiente. Pero recuerde que una Task no tiene una propiedad Result y que no se genera ningún valor cuando se aplica un operador await a una Task.

El código siguiente separa la llamada del método WaitAndApologizeAsync de la espera de la tarea que el método devuelve.

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 valor devuelto Task<TResult>

El tipo de valor devuelto Task<TResult> se usa para un método asincrónico que contiene una instrucción return en la que el operando es TResult.

En el ejemplo siguiente, el método GetLeisureHoursAsync contiene una instrucción return que devuelve un entero. La declaración del método debe tener un tipo de valor devuelto de Task<int>. El método asincrónico FromResult es un marcador de posición para una operación que devuelve una propiedad 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

Cuando se llama a GetLeisureHoursAsync desde una expresión await en el método ShowTodaysInfo, esta recupera el valor entero (el valor de leisureHours) que está almacenado en la tarea que devuelve el método GetLeisureHours. Para más información sobre las expresiones await, vea await.

Puede comprender mejor cómo await recupera el resultado de Task<T> si separa la llamada a GetLeisureHoursAsync de la aplicación de await, como se muestra en el código siguiente. Una llamada al método GetLeisureHoursAsync que no se espera inmediatamente devuelve Task<int>, como se podría esperar de la declaración del método. La tarea se asigna a la variable getLeisureHoursTask en el ejemplo. Dado que getLeisureHoursTask es Task<TResult>, contiene una propiedad Result de tipo TResult. En este caso, TResult representa un tipo entero. Cuando await se aplica a getLeisureHoursTask, la expresión await se evalúa en el contenido de la propiedad Result de getLeisureHoursTask. El valor se asigna a la variable ret.

Importante

La propiedad Result es una propiedad de bloqueo. Si se intenta acceder a ella antes de que termine su tarea, se bloquea el subproceso que está activo actualmente hasta que finaliza la tarea y el valor está disponible. En la mayoría de los casos, se debe tener acceso al valor usando await en lugar de tener acceso directamente a la propiedad.

En el ejemplo anterior se ha recuperado el valor de la propiedad Result para bloquear el subproceso principal de manera que el método Main pueda imprimir el mensaje (message) en la consola antes de que finalice la aplicación.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Tipo de valor devuelto Void

Usa el tipo de valor devuelto void en controladores de eventos asincrónicos, que necesitan un tipo de valor devuelto void. Para métodos que no sean controladores de eventos que no devuelven un valor, debe devolver Task en su lugar, porque no se espera a un método asincrónico que devuelva void. Cualquiera que realice la llamada a este método debe continuar hasta completarse sin esperar a que finalice el método asincrónico que se haya llamado. El autor de la llamada debe ser independiente de los valores o las excepciones que genere el método asincrónico.

El autor de la llamada de un método asincrónico que devuelva void no puede detectar las excepciones que inicia el método. Es probable que estas excepciones no controladas provoquen un error en la aplicación. Si un método que devuelve un valor Task o Task<TResult> inicia una excepción, la excepción se almacena en la tarea devuelta. La excepción se vuelve a iniciar cuando se espera a la tarea. Asegúrese de que cualquier método asincrónico que puede iniciar una excepción tiene un tipo de valor devuelto de Task o Task<TResult>, y que se esperan las llamadas al método.

En el ejemplo siguiente se muestra el comportamiento de un controlador de eventos asincrónicos. En el código de ejemplo, un controlador de eventos asincrónicos debe informar al subproceso principal de que ha terminado. Después, el subproceso principal puede esperar a que un controlador de eventos asincrónicos finalice antes de salir del 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 valor devueltos asincrónicos generalizados y ValueTask<TResult>

Un método asincrónico puede devolver cualquier tipo que tenga un método GetAwaiter accesible que devuelva una instancia de un tipo awaiter. Además, el tipo devuelto por el método GetAwaiter debe tener el atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Puede obtener más información en el artículo sobre Atributos leídos por el compilador o la especificación de C# para el Patrón del compilador de tipos de tareas.

Esta característica es el complemento de las expresiones con await, que describe los requisitos del operando de await. Los tipos de valor devueltos asincrónicos generalizados permiten al compilador generar métodos async que devuelven tipos diferentes. Los tipos de valor devueltos asincrónicos generalizados permitían mejoras de rendimiento en las bibliotecas de .NET. Como Task y Task<TResult> son tipos de referencia, la asignación de memoria en las rutas críticas para el rendimiento, especialmente cuando las asignaciones se producen en ajustados bucles, puede afectar negativamente al rendimiento. La compatibilidad para los tipos de valor devuelto generalizados significa que puede devolver un tipo de valor ligero en lugar de un tipo de referencia para evitar asignaciones de memoria adicionales.

.NET proporciona la estructura System.Threading.Tasks.ValueTask<TResult> como una implementación ligera de un valor de devolución de tareas generalizado. En el ejemplo siguiente se usa la estructura ValueTask<TResult> para recuperar el valor de dos tiradas de dado.

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

La escritura de un tipo de valor devuelto asincrónico generalizado es un escenario avanzado y está destinado para su uso en entornos especializados. En su lugar, considere la posibilidad de usar los tipos Task, Task<T> y ValueTask<T>, que abarcan la mayoría de los escenarios del código asincrónico.

En C# 10 y versiones posteriores, se puede aplicar el atributo AsyncMethodBuilder a un método asincrónico (en lugar de la declaración de tipo de valor devuelto asincrónico) para invalidar el generador de ese tipo. Normalmente, este atributo se aplica para usar un generador diferente proporcionado en el entorno de ejecución de .NET.

Secuencias asincrónicas con IAsyncEnumerable<T>

Un método asincrónico puede devolver una secuencia asincrónica, representada por IAsyncEnumerable<T>. Una secuencia asincrónica proporciona una manera de enumerar los elementos leídos de una secuencia cuando se generan elementos en fragmentos con llamadas asincrónicas repetidas. En el ejemplo siguiente se muestra un método asincrónico que genera una secuencia asincrónica:

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();
    }
}

En el ejemplo anterior, las líneas de una cadena se leen de forma asincrónica. Una vez que se ha leído cada línea, el código enumera cada palabra de la cadena. Los autores de la llamada enumerarían cada palabra mediante la instrucción await foreach. El método espera cuando necesita leer de forma asincrónica la línea siguiente de la cadena de origen.

Vea también