Partekatu honen bidez:


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 asíncrono que devuelve un flujo asíncrono .

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

Existen otros tipos que también 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 normalmente tienen un tipo de valor devuelto de Task. Estos métodos devuelven void si se ejecutan sincrónicamente. 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, por lo 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 un await es un 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. Sin embargo, recuerde que un Task no tiene una propiedad Result y que no se genera ningún valor cuando se aplica un operador await a un 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 de método debe especificar 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 un 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 dentro de una expresión await en el método ShowTodaysInfo, la expresión await recupera el valor entero (el valor de leisureHours) almacenado en la tarea devuelta por 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 un Task<T> separando 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 un 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 intenta acceder a ella antes de que finalice su tarea, el subproceso que está activo actualmente se bloquea hasta que se complete 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 recuperó el valor de la propiedad Result para bloquear el subproceso principal para que el método Main pudiera imprimir el message en la consola antes de que finalizara 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

Usas el tipo de retorno void en controladores de eventos asincrónicos, que requieren un tipo de retorno void. En el caso de los métodos distintos de los controladores de eventos que no devuelven un valor, debe devolver un Task en su lugar, porque no se puede esperar 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 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 Task o Task<TResult> produce 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ónico. En el código de ejemplo, un controlador de eventos asincrónico debe informar al subproceso principal cuando termine. A continuación, el subproceso principal puede esperar a que se complete un controlador de eventos asincrónico 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 retorno asincrónico 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 desde el método GetAwaiter debe tener el atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Puede obtener más información en el artículo sobre los atributos de leídos por el compilador o la especificación de C# para el patrón del generador de tipos de tarea .

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 retorno asincrónicos generalizados permitieron mejoras en el rendimiento de las bibliotecas de .NET. Dado que Task y Task<TResult> son tipos de referencia, la asignación de memoria en rutas de acceso críticas para el rendimiento, especialmente cuando se producen asignaciones en bucles estrechos, puede afectar negativamente al rendimiento. La compatibilidad con tipos de valor devuelto generalizados significa que puede devolver un tipo de valor ligero en lugar de un tipo de referencia para evitar más asignaciones de memoria.

.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 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

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

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, aplicaría este atributo 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 desde una secuencia cuando los elementos se generan 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 se leen líneas de una cadena de forma asincrónica. Una vez que se lee cada línea, el código enumera cada palabra de la cadena. Los llamadores enumerarían cada palabra mediante la instrucción await foreach. El método espera cuando necesita leer de forma asincrónica la siguiente línea de la cadena de origen.

Consulte también