Поделиться через


Асинхронные возвращаемые типы (C#)

Асинхронные методы могут иметь следующие типы возвращаемых данных:

  • Taskдля асинхронного метода, выполняющего операцию, но не возвращает значения.
  • Task<TResult>для асинхронного метода, возвращающего значение.
  • voidдля обработчика событий.
  • Любой тип, имеющий доступный метод GetAwaiter. Объект, возвращаемый методом GetAwaiter, должен реализовать интерфейс System.Runtime.CompilerServices.ICriticalNotifyCompletion.
  • IAsyncEnumerable<T>для асинхронного метода, возвращающего асинхронный поток.

Дополнительные сведения об асинхронных методах см. в асинхронном программировании с использованием async и await.

Существуют и другие типы, относящиеся к рабочим нагрузкам Windows:

  • DispatcherOperationдля асинхронных операций, ограниченных Windows.
  • IAsyncActionдля асинхронных действий в приложениях универсальной платформы Windows (UWP), которые не возвращают значение.
  • IAsyncActionWithProgress<TProgress>для асинхронных действий в приложениях UWP, которые сообщают о ходе выполнения, но не возвращают значение.
  • IAsyncOperation<TResult>для асинхронных операций в приложениях UWP, возвращающих значение.
  • IAsyncOperationWithProgress<TResult,TProgress>для асинхронных операций в приложениях UWP, которые сообщают о ходе выполнения и возвращают значение.

Тип возвращаемого значения задачи

Асинхронные методы, не содержащие инструкцию return или содержащие инструкцию return, которая не возвращает операнду, обычно имеет тип возвращаемого значения Task. Такие методы возвращают void, если они выполняются синхронно. Если для асинхронного метода используется тип возврата Task, вызывающий метод может использовать оператор await для приостановки завершения вызывающего метода до завершения вызываемого асинхронного метода.

В следующем примере метод WaitAndApologizeAsync не содержит инструкцию return, поэтому метод возвращает объект Task. Возврат Task позволяет ожидать WaitAndApologizeAsync. Тип Task не включает свойство Result, так как оно не имеет возвращаемого значения.

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 ожидается с использованием инструкции await, а не выражения await, аналогично вызову синхронного метода, возвращающего void. Приложение оператора await в данном случае не создает значение. Если правый операнд await является Task<TResult>, выражение await создает результат T. Когда правый операнд await является Task, await и его операнд являются выражением.

Вызов WaitAndApologizeAsync можно отделить от использования оператора await, например, в следующем коде. Однако помните, что Task не имеет свойства Result, и что значение не создается при применении оператора await к Task.

Следующий код отделяет вызов метода WaitAndApologizeAsync от ожидания задачи, возвращаемой методом.

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

Тип возврата<задачи TResult>

Тип Task<TResult> используется для асинхронного метода, который содержит оператор , возвращающий, в котором операнд — TResult.

В следующем примере метод GetLeisureHoursAsync содержит оператор return, возвращающий целое число. Объявление метода должно указывать тип возвращаемого значения Task<int>. Асинхронный метод FromResult — это заполнитель для операции, возвращающей 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

При вызове GetLeisureHoursAsync из выражения await в методе ShowTodaysInfo выражение await извлекает целочисленное значение (значение leisureHours), хранящееся в задаче, возвращаемой методом GetLeisureHours. Дополнительные сведения о выражениях await см. в разделе await.

Вы можете лучше понять, как await получает результат из Task<T>, отделив вызов к GetLeisureHoursAsync от применения await, как это показано в следующем коде. Вызов метода GetLeisureHoursAsync, который не ожидается сразу же, возвращает Task<int>, как ожидается из объявления метода. Задача назначается переменной getLeisureHoursTask в примере. Так как getLeisureHoursTask является Task<TResult>, он содержит свойство Result типа TResult. В этом случае TResult представляет целочисленный тип. Когда await применяется к getLeisureHoursTask, выражение await вычисляется в значение свойства Result объекта getLeisureHoursTask. Значение назначается переменной ret.

Важный

Свойство Result является блокирующим свойством. Если вы попытаетесь получить доступ к нему до завершения задачи, поток, который сейчас активен, будет заблокирован до окончания задачи и пока значение не станет доступным. В большинстве случаев необходимо получить доступ к значению с помощью await вместо прямого доступа к свойству.

В предыдущем примере было получено значение свойства Result для блокировки основного потока, чтобы метод Main мог распечатать message в консоль до завершения приложения.

var getLeisureHoursTask = GetLeisureHoursAsync();

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

Console.WriteLine(message);

Тип возвращаемого значения Void

Тип возврата void используется в асинхронных обработчиках событий, для которых требуется тип возврата void. Для методов, отличных от обработчиков событий, которые не возвращают значение, следует вернуть Task вместо этого, так как асинхронный метод, возвращающий void, не может быть ожидаем. Любой вызывающий метод должен продолжать выполняться, не ожидая завершения вызываемого асинхронного метода. Вызывающий объект должен быть независимым от любых значений или исключений, создаваемых асинхронным методом.

Вызывающий метод async, возвращающий void, не может перехватывать исключения, вызванные методом. Такие необработанные исключения могут привести к сбою приложения. Если метод, возвращающий Task или Task<TResult>, создает исключение, исключение сохраняется в возвращаемой задаче. Исключение бросается снова, когда ожидается завершение задачи. Убедитесь, что любой из асинхронных методов, который может вызвать исключение, имеет тип возвращаемого значения Task или Task<TResult> и что вызовы этого метода ожидаются.

В следующем примере показано поведение асинхронного обработчика событий. В примере кода асинхронный обработчик событий должен сообщить основному потоку о завершении. Затем основной поток может ожидать завершения асинхронного обработчика событий перед выходом из программы.

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.

Обобщенные асинхронные типы возвращаемых данных и ValueTask<TResult>

Асинхронный метод может возвращать любой тип, который имеет доступный метод GetAwaiter и возвращает экземпляр типа ожидателя . Кроме того, тип, возвращаемый методом GetAwaiter, должен иметь атрибут System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Дополнительные сведения см. в статье о атрибутах , прочитанных компилятором или спецификацией C# для шаблона построителя типов задач .

Эта функция является дополнением к ожидаемым выражениям, которые описывают требования для операнда await. Обобщенные асинхронные возвращаемые типы позволяют компилятору создавать методы async, возвращающие разные типы. Общие асинхронные типы возвращаемых значений позволили улучшить производительность в библиотеках .NET. Так как Task и Task<TResult> являются ссылочными типами, выделение памяти на производительно-значимых участках, особенно если выделение происходит в плотных циклах, может отрицательно повлиять на производительность. Поддержка обобщенных типов возвращаемых значений означает, что вместо ссылочного типа можно возвращать упрощенный тип значения, чтобы избежать выделения памяти.

.NET предоставляет структуру System.Threading.Tasks.ValueTask<TResult> как облегчённую реализацию обобщенного возвращаемого значения задач. В следующем примере используется структура ValueTask<TResult> для получения значений двух бросков кубиков.

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

Написание обобщённого асинхронного возвращаемого типа является сложным сценарием и предназначено для использования в специализированных средах. Вместо этого рекомендуется использовать Task, Task<T>и типы ValueTask<T>, которые охватывают большинство сценариев асинхронного кода.

Атрибут AsyncMethodBuilder можно применить к асинхронным методу (вместо объявления асинхронного возвращаемого типа), чтобы переопределить построитель для этого типа. Обычно этот атрибут применяется для использования другого построителя, предоставленного в среде выполнения .NET.

Асинхронные потоки с IAsyncEnumerable<T>

Асинхронный метод может возвращать асинхронный поток, представленный IAsyncEnumerable<T>. Асинхронный поток предоставляет способ перечисления элементов, считываемых из потока, когда элементы создаются частями с помощью повторяющихся асинхронных вызовов. В следующем примере показан асинхронный метод, который создает асинхронный поток:

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

В предыдущем примере строки считываются из текста асинхронно. После чтения каждой строки код перечисляет каждое слово в строке. Вызывающие будут перечислять каждое слово с помощью инструкции await foreach. Метод ожидает, когда необходимо асинхронно считывать следующую строку из исходной строки.

См. также