Partager via


Types de retour asynchrones (C#)

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

  • Task, pour une méthode asynchrone qui effectue une opération, mais ne retourne aucune valeur.
  • Task<TResult>, pour une méthode asynchrone qui retourne une valeur.
  • void, pour un gestionnaire d’événements.
  • Tout type possédant 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 asynchrones, consultez Programmation asynchrone avec async-await (C#).

Plusieurs autres types existent également qui sont spécifiques aux charges de travail Windows :

Type de retour de tâche

Les méthodes asynchrones qui ne contiennent pas d’instruction return ou qui contiennent une instruction return qui ne retourne pas d’opérande ont généralement un type de retour de Task. Ces méthodes retournent void si elles s’exécutent de manière synchrone. Si vous utilisez un type de retour Task pour une méthode asynchrone, une méthode appelante peut utiliser un opérateur await pour suspendre l’achèvement de l’appelant jusqu’à ce que la méthode asynchrone appelée se termine.

Dans l’exemple suivant, la méthode WaitAndApologizeAsync ne contient pas d’instruction return. Par conséquent, 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 d’un await est un 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. Toutefois, n’oubliez pas qu’une Task n’a pas de propriété Result et qu’aucune valeur n’est produite lorsqu’un opérateur Await est appliqué à un 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 de tâche <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 entier. La déclaration de méthode doit spécifier un type de retour de Task<int>. La méthode asynchrone FromResult est un substitut pour une opération qui retourne 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

Lorsque GetLeisureHoursAsync est appelée à partir d’une expression await dans la méthode ShowTodaysInfo, l’expression await récupère la valeur entière (la valeur de leisureHours) 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 d’un Task<T> en séparant l’appel à 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 affectée à la variable getLeisureHoursTask dans l’exemple. Étant donné que 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 affectée à la variable ret.

Importante

Result est une propriété bloquante. Si vous essayez d’y accéder avant la fin de sa tâche, le thread actuellement actif est bloqué jusqu’à ce que la tâche se termine et que la valeur soit 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 les 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, qui nécessitent 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 un Task à la place, car une méthode asynchrone 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 une Task ou Task<TResult> lève une exception, l’exception est stockée dans la tâche retournée. L’exception est lancé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 montre le comportement d’un gestionnaire d’événements asynchrone. Dans l’exemple de code, un gestionnaire d’événements asynchrone doit indiquer au thread principal une fois terminé. Ensuite, le thread principal peut attendre la fin d’un gestionnaire d’événements asynchrone 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 asynchrones 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 de référence, l’allocation de mémoire dans les chemins critiques de performances, en particulier lorsque des allocations se produisent dans des boucles serrées, peut affecter les performances. La prise en charge des 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 davantage d’allocations de mémoire.

.NET fournit la structure System.Threading.Tasks.ValueTask<TResult> en tant qu’implémentation légère d’une valeur de retour de tâche généralisée. L’exemple suivant utilise la structure ValueTask<TResult> pour récupérer la valeur de deux rouleaux de 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> à la place, qui couvrent la plupart des scénarios de code asynchrone.

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