Freigeben über


Asynchrone Rückgabetypen (C#)

Async-Methoden können die folgenden Rückgabetypen aufweisen:

  • 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 Event Handler.
  • 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 Stream zurückgibt.

Weitere Informationen zu asynchronen Methoden finden Sie unter Asynchrone Programmierung mit async und await (C#).

Es gibt auch verschiedene andere Typen, die für Windows-Workloads spezifisch sind:

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 werden zurückgegeben void , wenn sie synchron ausgeführt werden. Wenn Sie einen Task Rückgabetyp für eine asynchrone Methode verwenden, kann eine aufrufende Methode einen await Operator verwenden, um den Abschluss des Aufrufers auszusetzen, bis die aufgerufene asynchrone Methode abgeschlossen ist.

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

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 durch die Verwendung einer await-Anweisung anstelle eines await-Ausdrucks gewartet, ähnlich wie die aufrufende Anweisung für eine synchrone Methode mit Rückgabe eines Leerraums. Die Anwendung eines Await-Operators in diesem Fall erzeugt keinen Wert. Wenn der rechte Operand eines await ein Task<TResult> ist, erzeugt der await-Ausdruck ein Ergebnis von T. Wenn der rechte Operand eines await ein Task ist, sind das 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). Denken Sie jedoch daran, dass Task keine Result Eigenschaft hat und dass kein Wert erzeugt wird, wenn ein Await-Operator auf ein Task angewendet wird.

Der folgende Code trennt das Aufrufen der WaitAndApologizeAsync Methode vom Warten auf die 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 Task<TResult> Rückgabetyp wird für eine asynchrone Methode verwendet, die eine Rückgabe-Anweisung enthält, in der sich der Operand befindet TResult.

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 innerhalb eines Await-Ausdrucks in der ShowTodaysInfo Methode aufgerufen wird, ruft der Await-Ausdruck den ganzzahligen Wert (den Wert von leisureHours) ab, der in der von der GetLeisureHours Methode zurückgegebenen Aufgabe gespeichert ist. Weitere Informationen zu Await-Ausdrücken finden Sie unter await.

Sie können besser nachvollziehen, wie await das Ergebnis aus einem Task<T> abruft, indem Sie den Aufruf von GetLeisureHoursAsync von der Anwendung von await trennen, wie der folgende Code zeigt. Ein Aufruf der Methode GetLeisureHoursAsync, der nicht sofort erwartet wird, liefert, wie aus der Deklaration der Methode zu erwarten ist, einen Task<int> zurück. Die Aufgabe wird der getLeisureHoursTask Variablen im Beispiel zugewiesen. Weil getLeisureHoursTask eine Task<TResult> ist, enthält getLeisureHoursTask eine TResult-Eigenschaft vom Typ . In diesem Fall repräsentiert TResult einen ganzzahligen Typ. 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.

Von Bedeutung

Die Result Eigenschaft ist eine blockierungseigenschaft. Wenn Sie versuchen, darauf zuzugreifen, bevor die Aufgabe abgeschlossen ist, wird der aktuell aktive Thread blockiert, bis die Aufgabe abgeschlossen ist 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 Eigenschaft Result abgerufen, um den Hauptthread zu blockieren, damit die Methode Mainmessage auf 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“

Sie verwenden den void Rückgabetyp in asynchronen Ereignishandlern, die einen void Rückgabetyp erfordern. Für Methoden außer Ereignisbehandlern, die keinen Wert zurückgeben, sollten Sie stattdessen ein Task zurückgeben, da eine asynchrone Methode, die void zurückgibt, nicht genutzt werden kann. Jeder Aufrufer einer solchen Methode muss bis zum Ende fortfahren, ohne zu warten, dass die aufgerufene asynchrone Methode abgeschlossen ist. Der Aufrufer muss unabhängig von Werten oder Ausnahmen sein, die von der asynchronen Methode generiert werden.

Der Aufrufer einer "void-returning"-asynchronen Methode kann keine Ausnahmen abfangen, die von der Methode ausgelöst werden. Solche unbehandelten Ausnahmen führen wahrscheinlich dazu, dass Ihre Anwendung fehlschlägt. Wenn eine Methode, die ein Task oder Task<TResult> zurückgibt, eine Ausnahme wirft, wird diese Ausnahme in der zurückgegebenen Aufgabe 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, einen Rückgabetyp mit Task oder Task<TResult> hat und dass Aufrufe der Methode abgewartet werden.

Das folgende Beispiel zeigt das Verhalten eines asynchronen Ereignishandlers. Im Beispielcode muss ein asynchroner Ereignishandler den Hauptthread darüber informieren, wann er abgeschlossen ist. Anschließend kann der Hauptthread warten, bis ein asynchroner Ereignishandler abgeschlossen ist, 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 über eine barrierefreie GetAwaiter Methode verfügt, die eine Instanz eines Awaiter-Typs zurückgibt. Darüber hinaus muss der von der GetAwaiter Methode zurückgegebene Typ über das System.Runtime.CompilerServices.AsyncMethodBuilderAttribute Attribut verfügen. Weitere Informationen finden Sie im Artikel zu Attributen, die vom Compiler oder der C#-Spezifikation für das Aufgabentyp-Generator-Muster gelesen werden.

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 async von Methoden, die unterschiedliche Typen zurückgeben. Generalisierte asynchrone Rückgabetypen ermöglichen Leistungsverbesserungen in .NET-Bibliotheken. Da Task und Task<TResult> Referenztypen sind, kann die Speicherzuweisung in leistungskritischen Pfaden, insbesondere bei Zuordnungen in engen Schleifen, die Leistung erheblich beeinträchtigen. Die Unterstützung für generalisierte Rückgabetypen bedeutet, dass Sie einen einfachen Werttyp anstelle eines Verweistyps zurückgeben können, um mehr Speicherzuweisungen zu vermeiden.

.NET stellt die System.Threading.Tasks.ValueTask<TResult> Struktur als einfache Implementierung eines generalisierten Aufgabenrückgabewerts bereit. Im folgenden Beispiel wird die ValueTask<TResult> Struktur verwendet, um den Wert von zwei Würfelrollen 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 erweitertes Szenario und dient zur Verwendung in spezialisierten Umgebungen. Erwägen Sie stattdessen die Verwendung der TaskTypen , Task<T>ValueTask<T> die die meisten Szenarien für asynchronen Code abdecken.

Sie können das AsyncMethodBuilder Attribut auf eine asynchrone Methode (anstelle der asynchronen Rückgabetypdeklaration) anwenden, um den Generator für diesen Typ außer Kraft zu setzen. In der Regel wenden Sie dieses Attribut an, um einen anderen Builder zu verwenden, der in der .NET-Laufzeit bereitgestellt wird.

Asynchrone Datenströme mit IAsyncEnumerable<T>

Eine asynchrone Methode gibt möglicherweise einen asynchronen Datenstrom zurück, dargestellt durch IAsyncEnumerable<T>. Ein asynchroner Stream bietet eine Möglichkeit zum Aufzählen von Elementen, die aus einem Datenstrom gelesen werden, wenn Elemente in Blöcken mit wiederholten asynchronen Aufrufen generiert werden. Das folgende Beispiel zeigt eine asynchrone Methode, die einen asynchronen Stream 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 jede Zeile gelesen wurde, listet der Code jedes Wort in der Zeichenfolge auf. Aufrufer würden jedes Wort mithilfe der await foreach Anweisung aufzählen. Die Methode wartet, wenn sie die nächste Zeile asynchron aus der Quellzeichenfolge lesen muss.

Siehe auch