异步返回类型 (C#)

异步方法可以具有以下返回类型:

有关异步方法的详细信息,请参阅 使用 async 和 await 的异步编程(C#)

其他几种类型也特定于 Windows 工作负载:

任务返回类型

不包含 return 语句或包含不返回操作数的 return 语句的异步方法通常具有 Task的返回类型。 如果此类方法以同步方式运行,则返回 void。 如果在异步方法中使用 Task 返回类型,调用方法可以使用 await 运算符暂停调用方的完成,直至被调用的异步方法结束。

在下面的示例中,WaitAndApologizeAsync 方法不包含 return 语句,因此该方法返回 Task 对象。 返回 Task 可等待 WaitAndApologizeAsyncTask 类型不包含 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.

通过使用 await 语句而不是 await 表达式等待 WaitAndApologizeAsync,类似于返回 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);

Task<TResult> 返回类型

Task<TResult> 返回类型用于某种异步方法,此异步方法包含 return 语句,其中操作数是 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 变量。 由于 getLeisureHoursTaskTask<TResult>,因此它包含 Result类型的 TResult 属性。 在这种情况下,TResult 表示整数类型。 await 应用于 getLeisureHoursTask,await 表达式的计算结果为 ResultgetLeisureHoursTask 属性内容。 该值分配给 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 的异步方法。 调用此类方法的任何调用方都必须继续执行,而不必等待被调用的异步方法完成。 调用方必须独立于异步方法生成的任何值或异常。

Void 返回异步方法的调用方无法捕获从该方法引发的异常。 此类未经处理的异常可能会导致应用程序失败。 如果返回 TaskTask<TResult> 的方法引发异常,异常将存储在返回的任务中。 等待任务时,将重新引发异常。 确保可以生成异常的任何异步方法的返回类型为 TaskTask<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>

异步方法可以返回具有返回 awaiterGetAwaiter 类型实例的可访问 方法的所有类型。 此外,返回的类型必须与 SetResult 的参数类型和 Task 属性指定的类型上的 System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 属性的返回类型匹配。 可以通过有关编译器读取的属性的文章或任务类型生成器模式的 C# 规范,了解详细信息。

此功能与 awaitable 表达式相辅相成,后者描述 await 操作数的要求。 通用异步返回类型使编译器能够生成返回不同类型的 async 方法。 泛化的异步返回类型促进了 .NET 库中的性能改进。 由于 TaskTask<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

编写通用异步返回类型是一种高级方案,旨在用于专用环境。 请考虑改用 TaskTask<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 语句枚举每个单词。 该方法在需要从源字符串异步读取下一行时等待。

另请参阅