Udostępnij za pośrednictwem


Typy zwracane asynchroniczne (C#)

Metody asynchroniczne mogą mieć następujące typy zwracanych danych.

  • Task, dla metody asynchronicznej, która wykonuje operację, ale nie zwraca żadnej wartości.
  • Task<TResult>, dla metody asynchronicznej, która zwraca wartość.
  • void, dla programu obsługi zdarzeń.
  • Dowolny typ, który ma dostępną GetAwaiter metodę. Obiekt zwrócony przez metodę GetAwaiter musi zaimplementować interfejs System.Runtime.CompilerServices.ICriticalNotifyCompletion.
  • IAsyncEnumerable<T>, dla metody asynchronicznej zwracającej strumień asynchroniczny.

Aby uzyskać więcej informacji na temat metod asynchronicznych, zobacz Asynchroniczne programowanie z użyciem async i await (C#).

Istnieje również kilka innych typów specyficznych dla obciążeń systemu Windows:

Typ zwracania zadania

Metody asynchroniczne, które nie zawierają return instrukcji lub które zawierają instrukcję return , która nie zwraca operandu, zwykle ma zwracany typ Task. Takie metody zwracają void , jeśli działają synchronicznie. Jeśli używasz typu zwrotnego Task dla metody asynchronicznej, metoda wywołująca może użyć operatora await do wstrzymania zakończenia, dopóki wywoływana metoda asynchroniczna się nie zakończy.

W poniższym przykładzie metoda WaitAndApologizeAsync nie zawiera instrukcji return, więc zwraca obiekt Task. Zwrócenie Task umożliwia oczekiwanie na WaitAndApologizeAsync. Typ Task nie zawiera Result właściwości, ponieważ nie ma zwracanej wartości.

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 jest oczekiwane za pomocą instrukcji await zamiast wyrażenia await, podobnie jak instrukcja wywołania dla metody synchronicznej zwracającej void. Zastosowanie operatora await w tym przypadku nie generuje wartości. Gdy prawy operand elementu await to Task<TResult>, await wyrażenie generuje wynik T. Gdy prawy operand elementu await to Task, await i jego operand są instrukcją.

Wywołanie WaitAndApologizeAsync można oddzielić od zastosowania operatora await, co pokazuje poniższy kod. Należy jednak pamiętać, że Task nie ma właściwości Result i że żadna wartość nie jest generowana, gdy operator await jest stosowany do Task.

Poniższy kod oddziela wywołanie WaitAndApologizeAsync metody przed oczekiwaniem na zadanie zwracane przez metodę.

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

Typ zwracany zadania<TResult>

Typ zwracany Task<TResult> jest używany dla metody asynchronicznej, która zawiera instrukcję return, w której operand to TResult.

W poniższym przykładzie metoda GetLeisureHoursAsync zawiera instrukcję return zwracającą liczbę całkowitą. Deklaracja metody musi określać typ zwracany .Task<int> Metoda FromResult asynchroniczna jest symbolem zastępczym dla operacji zwracającej 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

Gdy GetLeisureHoursAsync jest wywoływana z wewnątrz wyrażenia await w metodzie ShowTodaysInfo , wyrażenie await pobiera wartość całkowitą (wartość leisureHours) przechowywaną w zadaniu zwróconym przez metodę GetLeisureHours . Aby uzyskać więcej informacji na temat wyrażeń await, zobacz await.

Lepiej zrozumieć, jak await pobiera wynik z obiektu Task<T> , oddzielając wywołanie metody GetLeisureHoursAsync od aplikacji await, jak pokazano w poniższym kodzie. Wywołanie metody GetLeisureHoursAsync, która nie jest natychmiast oczekiwana, zwraca wartość Task<int>, co jest zgodne z deklaracją metody. Zadanie jest przypisywane do zmiennej getLeisureHoursTask w przykładzie. Ponieważ getLeisureHoursTask jest Task<TResult>, posiada właściwość Result typu TResult. W tym przypadku TResult reprezentuje typ liczby całkowitej. Gdy await jest stosowany do getLeisureHoursTaskelementu , wyrażenie await oblicza zawartość Result właściwości getLeisureHoursTask. Wartość jest przypisywana do zmiennej ret .

Ważne

Właściwość Result jest właściwością blokującą. Jeśli spróbujesz uzyskać do niego dostęp przed zakończeniem zadania, wątek, który jest aktualnie aktywny, zostanie zablokowany, aż zadanie zostanie zakończone i wartość stanie się dostępna. W większości przypadków należy uzyskać dostęp do wartości przy użyciu metody await zamiast bezpośrednio uzyskiwać dostęp do właściwości.

Poprzedni przykład pobrał wartość Result właściwości, aby zablokować główny wątek, tak żeby metoda Main mogła wyświetlić message na konsoli przed zakończeniem aplikacji.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Typ zwracany void

Zwracany void typ jest używany w asynchronicznych programach obsługi zdarzeń, które wymagają zwracanego void typu. W przypadku metod innych niż programy obsługi zdarzeń, które nie zwracają wartości, należy zamiast tego zwrócić Task, ponieważ metody asynchroniczne zwracające void nie mogą być oczekiwane. Każdy obiekt wywołujący taką metodę musi kontynuować działanie aż do zakończenia, bez oczekiwania na zakończenie wywoływanej metody asynchronicznej. Obiekt wywołujący musi być niezależny od wartości i wyjątków generowanych przez metodę asynchroniczną.

Wywołujący metody asynchronicznej, która zwraca void, nie może przechwytywać wyjątków rzucanych przez tę metodę. Takie nieobsługiwane wyjątki mogą spowodować niepowodzenie aplikacji. Jeśli metoda, która zwraca Task lub Task<TResult>, zgłasza wyjątek, wyjątek ten jest przechowywany w zwracanym zadaniu. Wyjątek jest zmieniany, gdy zadanie jest oczekiwane. Upewnij się, że każda metoda asynchroniczna, która może wygenerować wyjątek, ma typ zwracany Task lub Task<TResult> oraz że wszystkie wywołania tej metody są oczekiwane.

W poniższym przykładzie pokazano zachowanie asynchronicznego programu obsługi zdarzeń. W przykładowym kodzie program obsługi zdarzeń asynchronicznych musi poinformować główny wątek o zakończeniu. Następnie główny wątek może poczekać na ukończenie programu obsługi zdarzeń asynchronicznych przed zamknięciem programu.

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.

Zunifikowane typy zwracane asynchroniczne i ValueTask<TResult>

Metoda asynchronizna może zwracać dowolny typ, który ma dostępną GetAwaiter metodę zwracającą wystąpienie typu awaiter. Ponadto typ zwracany z GetAwaiter metody musi mieć System.Runtime.CompilerServices.AsyncMethodBuilderAttribute atrybut . Więcej informacji można dowiedzieć się w artykule Dotyczącym atrybutów odczytanych przez kompilator lub specyfikację języka C# dla wzorca konstruktora typów zadań.

Ta funkcja jest uzupełnieniem dla wyrażeń oczekujących, które określają wymagania dla operandu await. Uogólnione typy zwracane asynchroniczne umożliwiają kompilatorowi generowanie async metod, które zwracają różne typy. Uogólnione typy zwracane asynchroniczne umożliwiają ulepszenia wydajności w bibliotekach platformy .NET. Ponieważ Task i Task<TResult> są typami referencyjnymi, alokacja pamięci w ścieżkach o krytycznym znaczeniu dla wydajności, szczególnie gdy alokacje występują w ciasnych pętlach, mogą mieć negatywny wpływ na wydajność. Obsługa uogólnionych typów zwracanych oznacza, że można zwrócić lekki typ wartości zamiast typu odwołania, aby uniknąć dodatkowych alokacji pamięci.

Platforma .NET udostępnia System.Threading.Tasks.ValueTask<TResult> strukturę jako uproszczoną implementację uogólnionej wartości zwracanej przez zadanie. W poniższym przykładzie użyto ValueTask<TResult> struktury do pobrania wartości dwóch rzutów kostkami.

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

Tworzenie uogólnionego typu zwrotnego dla operacji asynchronicznych jest zaawansowanym scenariuszem, przeznaczonym do użycia w dedykowanych środowiskach. Rozważ użycie typów Task, Task<T> i ValueTask<T>, które obejmują większość scenariuszy dla kodu asynchronicznego zamiast tego.

Atrybut AsyncMethodBuilder można zastosować do metody asynchronicznej (zamiast deklaracji typu zwrotnego asynchronicznego), aby zastąpić konstruktora tego typu. Zazwyczaj ten atrybut należy zastosować, aby użyć innego konstruktora udostępnionego w środowisku uruchomieniowym platformy .NET.

Strumienie asynchroniczne z funkcją IAsyncEnumerable<T>

Metoda asynchroniczna może zwracać strumień asynchroniczny, reprezentowany przez IAsyncEnumerable<T>. Strumień asynchroniczny umożliwia wyliczanie elementów odczytywanych ze strumienia, gdy elementy są generowane we fragmentach z powtarzającymi się wywołaniami asynchronicznymi. W poniższym przykładzie przedstawiono metodę asynchroniową, która generuje strumień asynchroniczny:

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

Powyższy przykład odczytuje wiersze z ciągu asynchronicznie. Po odczytaniu każdego wiersza kod wylicza każde słowo w ciągu. Dzwoniący wyliczają każde słowo przy użyciu instrukcji await foreach. Metoda oczekuje, gdy musi asynchronicznie odczytać następny wiersz z ciągu źródłowego.

Zobacz także