Types de retour asynchrone (C#)

Les méthodes async peuvent avoir les types de retour suivants :

  • Task, pour une méthode async qui effectue une opération mais ne retourne aucune valeur.
  • Task<TResult>, pour une méthode async qui retourne une valeur.
  • void, pour un gestionnaire d’événements.
  • Tout type ayant une méthode GetAwaiter accessible. L’objet retourné par la méthode GetAwaiter doit implémenter l’interface System.Runtime.CompilerServices.ICriticalNotifyCompletion.
  • IAsyncEnumerable<T>, pour une méthode asynchrone qui retourne un flux asynchrone.

Pour plus d’informations sur les méthodes async, consultez Programmation asynchrone avec async et await (C#).

Il existe plusieurs autres types qui sont spécifiques aux charges de travail Windows :

Type de retour Task

Les méthodes async qui ne contiennent pas d’instruction return ou qui contiennent une instruction return ne retournant pas d’opérande ont généralement un type de retour Task. De telles méthodes retournent void si elles s’exécutent de façon synchrone. Si vous utilisez un type de retour Task pour une méthode async, une méthode d’appel peut utiliser un opérateur await pour suspendre l’achèvement de l’appelant jusqu’à ce que la méthode async soit terminée.

Dans l’exemple suivant, la méthode WaitAndApologizeAsync ne contient pas d’instruction return, de sorte que la méthode retourne un objet Task. Le retour de Task permet à WaitAndApologizeAsync d’être attendu. Le type Task n’inclut pas de propriété Result, car il n’a aucune valeur de retour.

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 est attendue avec une instruction await au lieu d’une expression await, comme pour l’instruction d’appel d’une méthode synchrone retournant un type void. Dans ce cas, l’application d’un opérateur await ne génère pas de valeur. Lorsque l’opérande droit de await est Task<TResult>, l’expression await produit un résultat de T. Lorsque l’opérande droit de await est Task, le await et son opérande sont une instruction.

Vous pouvez séparer l’appel vers WaitAndApologizeAsync de l’application d’un opérateur await, comme le montre le code suivant. Cependant, n’oubliez pas qu’un type Task n’a pas de propriété Result et qu’aucune valeur n’est générée quand un opérateur await est appliqué à un type Task.

Le code suivant sépare l’appel à la méthode WaitAndApologizeAsync de l’attente de la tâche que retourne la méthode.

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

Type de retour Task<TResult>

Le type de retour Task<TResult> est utilisé pour une méthode asynchrone qui contient une instruction return dans laquelle l’opérande est TResult.

Dans l’exemple suivant, la méthode GetLeisureHoursAsync contient une instruction return qui retourne un nombre entier. La déclaration de méthode doit spécifier un type de retour de Task<int>. La méthode asynchrone FromResult est un espace réservé pour une opération qui retourne 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

Quand la méthode GetLeisureHoursAsync est appelée à partir d’une expression await dans la méthode ShowTodaysInfo, cette expression récupère la valeur entière (la valeur de leisureHours) qui est stockée dans la tâche retournée par la méthode GetLeisureHours. Pour plus d’informations sur les expressions await, consultez await.

Vous pouvez mieux comprendre comment await récupère le résultat à partir de Task<T> en séparant l’appel vers GetLeisureHoursAsync de l’application de await, comme le montre le code suivant. Un appel à la méthode GetLeisureHoursAsync qui n’est pas immédiatement attendue retourne un type Task<int>, comme vous pourriez l’attendre de la déclaration de la méthode. La tâche est assignée à la variable getLeisureHoursTask dans l’exemple. Comme getLeisureHoursTask est un Task<TResult>, il contient une propriété Result de type TResult. Dans ce cas, TResult représente un type entier. Quand await est appliqué à getLeisureHoursTask, l’expression await prend pour la valeur le contenu de la propriété Result de getLeisureHoursTask. La valeur est assignée à la variable ret.

Important

Result est une propriété bloquante. Si vous essayez d’y accéder avant la fin de sa tâche, le thread actif est bloqué tant que la tâche n’est pas terminée et que la valeur n’est pas disponible. Dans la plupart des cas, vous devez accéder à la valeur avec await au lieu d’accéder directement à la propriété.

L’exemple précédent a récupéré la valeur de la propriété Result pour bloquer le thread principal afin que la méthode Main puisse imprimer message dans la console avant la fin de l’application.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Type de retour void

Vous utilisez le type de retour void dans les gestionnaires d’événements asynchrones, lesquels exigent un type de retour void. Pour les méthodes autres que les gestionnaires d’événements qui ne retournent pas de valeur, vous devez retourner Task à la place, car une méthode async qui retourne void ne peut pas être attendue. Tout appelant de cette méthode doit continuer jusqu’à la fin sans attendre la méthode asynchrone appelée. L’appelant doit être indépendant de toutes les valeurs ou exceptions générées par la méthode asynchrone.

L’appelant d’une méthode asynchrone de retour void ne peut pas intercepter les exceptions levées à partir de la méthode. Ces exceptions non gérées sont susceptibles d’entraîner l’échec de votre application. Si une méthode qui retourne Task ou Task<TResult> lève une exception, celle-ci est stockée dans la tâche retournée. L’exception est levée à nouveau lorsque la tâche est attendue. Assurez-vous que toute méthode asynchrone qui peut produire une exception a un type de retour de Task ou de Task<TResult> et que les appels à la méthode sont attendus.

L’exemple suivant illustre le comportement d’un gestionnaire d’événements asynchrones. Dans l’exemple de code, un gestionnaire d’événements asynchrones doit informer le thread principal lorsqu’il se termine. Le thread principal peut donc attendre la fin d’un gestionnaire d’événements asynchrones avant de quitter le programme.

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.

Types de retour async généralisés et ValueTask<TResult>

Une méthode asynchrone peut retourner n’importe quel type ayant une méthode GetAwaiter accessible qui retourne une instance d’un type awaiter. En outre, le type retourné à partir de la méthode GetAwaiter doit avoir l’attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Vous pouvez en savoir plus dans l’article sur les Attributs lus par le compilateur ou la spécification C# pour le Modèle de générateur de types de tâches.

Cette fonctionnalité est le complément d’expressions await qui décrit les exigences de l’opérande de await. Les types de retour asynchrones généralisés permettent au compilateur de générer des méthodes async qui retournent différents types. Les types de retour asynchrones généralisés ont permis d’améliorer les performances dans les bibliothèques .NET. Étant donné que Task et Task<TResult> sont des types référence, l’allocation de mémoire dans des chemins critiques pour les performances, en particulier quand les allocations se produisent dans des boucles serrées, peuvent nuire aux performances. La prise en charge de types de retour généralisés signifie que vous pouvez retourner un type valeur léger au lieu d’un type référence pour éviter des allocations de mémoire supplémentaires.

Le .NET fournit la structure System.Threading.Tasks.ValueTask<TResult> comme implémentation légère d’une valeur retournant des tâches généralisées. L’exemple suivant utilise la structure ValueTask<TResult> pour récupérer la valeur de deux dés.

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

L’écriture d’un type de retour asynchrone généralisé est un scénario avancé et est ciblé pour une utilisation dans des environnements spécialisés. Envisagez d’utiliser les types Task, Task<T>et ValueTask<T>, qui couvrent la plupart des scénarios de code asynchrone.

En C# 10 et versions ultérieures, vous pouvez appliquer l’attribut AsyncMethodBuilder à une méthode asynchrone (au lieu de la déclaration de type de retour asynchrone) pour remplacer le générateur pour ce type. En règle générale, vous appliquez cet attribut pour utiliser un autre générateur fourni dans le runtime .NET.

Flux asynchrones avec IAsyncEnumerable<T>

Une méthode asynchrone peut retourner un flux asynchrone, représenté par IAsyncEnumerable<T>. Un flux asynchrone permet d’énumérer les éléments lus à partir d’un flux lorsque des éléments sont générés en blocs avec des appels asynchrones répétés. L’exemple suivant montre une méthode asynchrone qui génère un flux asynchrone :

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

L’exemple précédent lit les lignes d’une chaîne de manière asynchrone. Une fois chaque ligne lue, le code énumère chaque mot de la chaîne. Les appelants énumèrent chaque mot à l’aide de l’instruction await foreach. La méthode attend quand elle doit lire de manière asynchrone la ligne suivante à partir de la chaîne source.

Voir aussi