Asynchrone Rückgabetypen (C#)

Asynchrone Methoden können folgende Rückgabetypen haben:

  • Task für eine asynchrone Methode, die einen Vorgang ausführt, aber keinen Wert zurückgibt.
  • Task<TResult> für eine asynchrone Methode, die einen Wert zurückgibt.
  • void für einen Ereignishandler.
  • Jeder Typ verfügt über eine zugängliche GetAwaiter-Methode. Das von der GetAwaiter-Methode zurückgegebene Objekt muss die System.Runtime.CompilerServices.ICriticalNotifyCompletion-Schnittstelle implementieren.
  • IAsyncEnumerable<T> für eine asynchrone Methode, die einen asynchronen Datenstrom zurückgibt.

Weitere Informationen über async-Methoden finden Sie unter Asynchrone Programmierung mit async und await (C#).

Es gibt auch einige weitere Typen, die spezifisch für Windows-Workloads gelten:

Task-Rückgabetyp

Asynchrone Methoden, die keine return-Anweisung enthalten oder eine return-Anweisung enthalten, die keinen Operanden zurückgibt, haben normalerweise einen Rückgabetyp von Task. Solche Methoden geben void zurück, wenn sie synchron ausgeführt werden. Wenn Sie einen Task-Rückgabetyp für eine asynchrone Methode verwenden, kann ein aufrufende Methode einen await-Operator verwenden, um den Abschluss des Aufrufers anzuhalten, bis die aufgerufene asynchrone Methode beendet ist.

Im folgenden Beispiel enthält die WaitAndApologizeAsync-Methode keine return-Anweisung, weshalb die Methode ein Task-Objekt zurückgibt. Durch Rückgabe von Task wird ermöglicht, dass WaitAndApologizeAsync erwartet wird. Der Typ Task enthält keine Result-Eigenschaft, da er nicht über einen Rückgabewert verfügt.

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 wird erwartet, indem eine „await“-Anweisung anstelle eines „await“-Ausdrucks verwendet wird, ähnlich der Aufrufanweisung einer Methode, die „void“ zurückgibt. Die Anwendung eines Erwartungsoperators erzeugt in diesem Fall keinen Wert. Wenn der rechte Operand von awaitTask<TResult> ist, generiert der Ausdruck await ein Ergebnis von T. Wenn der rechte Operand von awaitTask ist, sind await und sein Operand eine Anweisung.

Sie können den WaitAndApologizeAsync-Aufruf eines await-Operators von der Anwendung trennen (dies wird im folgenden Code veranschaulicht). Beachten Sie jedoch, dass Task über keine Result-Eigenschaft verfügt und dass kein Wert erzeugt wird, wenn ein Erwartungsoperator auf Task angewendet wird.

Der folgende Code trennt Aufrufe der Methode WaitAndApologizeAsync vom Erwarten der Aufgabe, die die Methode zurückgibt.

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

Rückgabetyp „Task<TResult>“

Der Rückgabetyp Task<TResult> wird für eine asynchrone Methode verwendet, die eine return-Anweisung enthält, in der der Operand TResult lautet.

Im folgenden Beispiel enthält die GetLeisureHoursAsync-Methode eine return-Anweisung, die eine ganze Zahl zurückgibt. Die Methodendeklaration muss den Rückgabetyp Task<int> angeben. Die asynchrone Methode FromResult ist ein Platzhalter für einen Vorgang, der einen DayOfWeek-Wert zurückgibt.

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

Wenn GetLeisureHoursAsync aus einem „await“-Ausdruck in der Methode ShowTodaysInfo aufgerufen wird, ruft der „await“-Ausdruck den ganzzahligen Wert ab (der Wert von GetLeisureHours), der in der Aufgabe gespeichert wird, die von der Methode leisureHours zurückgegeben wird. Weitere Informationen zu await-Ausdrücken finden Sie unter await.

Wie await das Ergebnis aus einem Task<T> abruft, lässt sich besser erkennen, wenn Sie den Aufruf von GetLeisureHoursAsync von der Anwendung von await trennen, wie der folgende Code zeigt. Ein Aufruf der GetLeisureHoursAsync-Methode, die nicht sofort eine Antwort erwartet, gibt ein Task<int> zurück, wie Sie es von der Deklaration der Methode erwarten. Die Aufgabe wird im Beispiel der getLeisureHoursTask-Variablen zugewiesen. Da getLeisureHoursTask eine Task<TResult> ist, enthält es eine Result-Eigenschaft des Typs TResult. In diesem Fall stellt TResult einen Integertyp dar. Wenn await auf getLeisureHoursTask angewendet wird, wertet der „await“-Ausdruck den Inhalt der Eigenschaft Result von getLeisureHoursTask aus. Der Wert wird der ret-Variablen zugewiesen.

Wichtig

Die Result-Eigenschaft ist eine Blocking-Eigenschaft. Wenn Sie darauf zuzugreifen versuchen, bevor seine Aufgabe beendet ist, wird der momentan aktive Thread blockiert, bis die Aufgabe abgeschlossen und der Wert verfügbar ist. In den meisten Fällen sollten Sie auf den Wert zugreifen, indem Sie await verwenden, anstatt direkt auf die Eigenschaft zuzugreifen.

Im vorherigen Beispiel wurde der Wert der Result-Eigenschaft abgerufen, um den Hauptthread zu blockieren, damit die Main-Methode message an die Konsole ausgeben konnte, bevor die Anwendung beendet wurde.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Rückgabetyp „Void“

Der Rückgabetyp void wird in asynchronen Ereignishandlern verwendet, die den Rückgabetyp void erfordern. Für andere Methoden als Ereignishandler, die keinen Wert zurückgeben, sollten Sie stattdessen Task zurückgeben, da eine asynchrone Methode, die void zurückgibt, nicht erwartet werden kann. Jeder Aufrufer einer solchen Methode muss bis zum Abschluss ausgeführt werden, ohne darauf zu warten, dass die aufgerufene asynchrone Methode abgeschlossen wird. Der Aufrufer muss von allen Werten oder Ausnahmen unabhängig sein, die von der asynchronen Methode generiert werden.

Der Aufrufer einer asynchronen Methode, die „void“ zurückgibt, kann keine von der Methode ausgelösten Ausnahmen abfangen. Solche nicht behandelten Ausnahmen führen wahrscheinlich zu Fehlern in der Anwendung. Wenn eine Methode, die Task oder Task<TResult> zurückgibt, eine Ausnahme auslöst, wird die Ausnahme im zurückgegebenen Task gespeichert. Die Ausnahme wird erneut ausgelöst, wenn auf den Task gewartet wird. Stellen Sie sicher, dass jede asynchrone Methode, die eine Ausnahme erzeugen kann, den Rückgabetyp Task oder Task<TResult> aufweist, und dass Aufrufe der Methode erwartet werden.

Im folgenden Beispiel wird das Verhalten eines asynchronen Ereignishandlers dargestellt. Im Beispielcode muss ein asynchroner Ereignishandler dem Hauptthread mitteilen, dass er abgeschlossen wurde. Anschließend kann der Hauptthread darauf warten, dass ein asynchroner Ereignishandler abgeschlossen wird, bevor das Programm beendet wird.

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.

Generalisierte asynchrone Rückgabetypen und ValueTask<TResult>

Eine asynchrone Methode kann jeden Typ zurückgeben, der eine zugängliche GetAwaiter-Methode hat, die eine Instanz eines awaiter-Typs zurückgibt. Außerdem muss der von der GetAwaiter-Methode zurückgegebene Typ über das System.Runtime.CompilerServices.AsyncMethodBuilderAttribute-Attribut verfügen. Weitere Informationen finden Sie im Artikel Vom Compiler gelesene Attribute oder in der C#-Spezifikation für Taskartige Generatormuster.

Diese Funktion ist das Gegenstück zu awaitable Ausdrücken, die die Anforderungen an den Operanden von await beschreibt. Generalisierte asynchrone Rückgabetypen ermöglichen dem Compiler das Generieren von async-Methoden, die unterschiedliche Typen zurückgeben. Generalisierte asynchrone Rückgabetypen ermöglichten Leistungsverbesserungen in den .NET-Bibliotheken. Da es sich bei Task und Task<TResult> um Verweistypen handelt, kann bei der Speicherbelegung in leistungskritischen Pfaden, besonders bei Zuordnungen in engen Schleifen, die Leistung beeinträchtigt werden. Die Unterstützung für generalisierte Rückgabetypen bedeutet, dass Sie einen einfachen Werttyp statt eines Verweistypen zurückgeben können, um zusätzliche Speicherbelegungen zu vermeiden.

.NET stellt die Struktur System.Threading.Tasks.ValueTask<TResult> als einfache Implementierung eines generalisierten Werts bereit, der eine Aufgabe zurückgibt. Im folgenden Beispiel wird die Struktur ValueTask<TResult> verwendet, um den Wert von zwei Würfelvorgängen abzurufen.

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

Das Schreiben eines generalisierten asynchronen Rückgabetyps ist ein fortgeschrittenes Szenario und zur Verwendung in spezialisierten Umgebungen gedacht. Erwägen Sie stattdessen die Verwendung der Typen Task, Task<T> und ValueTask<T>, die die meisten Szenarios für asynchronen Code abdecken.

Ab C# 10 können Sie das AsyncMethodBuilder-Attribut auf eine asynchrone Methode (anstelle der Deklaration des asynchronen Rückgabetyps) anwenden, um den Generator für diesen Typ zu überschreiben. Normalerweise wenden Sie dieses Attribut an, um einen anderen Generator zu verwenden, der in der .NET-Runtime bereitgestellt wird.

Asynchrone Datenströme mit IAsyncEnumerable<T>

Eine asynchrone Methode kann einen asynchronen Datenstrom zurückgeben, der durch IAsyncEnumerable<T> dargestellt wird. Ein asynchroner Datenstrom bietet eine Möglichkeit zur Aufzählung von Elementen, die aus einem Datenstrom gelesen wurden, wenn die Elemente mit wiederholten asynchronen Aufrufen in Blöcken generiert werden. Das folgende Beispiel zeigt eine asynchrone Methode, die einen asynchronen Datenstrom generiert:

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

Das oben gezeigte Beispiel liest asynchron Zeilen aus einer Zeichenfolge. Sobald eine Zeile gelesen wurde, zählt der Code jedes Wort in der Zeichenfolge auf. Aufrufen zählen die einzelnen Wörter mit der await foreach-Anweisung auf. Die Methode wartet, wenn sie die nächste Zeile aus der Quellzeichenfolge asynchron lesen muss.

Siehe auch