非同步方法的傳回型別 (C#)

非同步方法可有下列傳回型別:

如需非同步方法的詳細資訊,請參閱使用 async 和 await 進行非同步程式設計 (C#)

Windows 工作負載也存在幾種其他類型:

工作傳回型別

不包含 return 陳述式的非同步方法,或包含不會傳回運算元的 return 陳述式的非同步方法,通常具有傳回型別 Task。 這類方法如果以同步方式執行,會傳回 void。 如果您針對非同步方法使用 Task 傳回型別,則除非被呼叫的非同步方法完成,否則呼叫的方法可以使用 await 運算子暫止呼叫端完成。

在下列範例中,WaitAndApologizeAsync 方法不包含 return 陳述式,所以方法會傳回 Task 物件。 傳回 Task 可讓 WaitAndApologizeAsync 受到等候 (await)。 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);

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

ShowTodaysInfo 方法的 await 運算式內呼叫 GetLeisureHoursAsync 時,await 運算式會擷取儲存在 GetLeisureHours 方法所傳回之工作中的整數值 (leisureHours 的值)。 如需 await 運算式的詳細資訊,請參閱 await

您可以藉由將 GetLeisureHoursAsync 呼叫與 await 的應用區分開來,深入了解 awaitTask<T> 擷取結果的運作方式,如下列程式碼所示。 呼叫不會立即等候的 GetLeisureHoursAsync 方法,會傳回 Task<int>,如您所預期的方法宣告。 在範例中,工作會指派給 getLeisureHoursTask 變數。 因為 getLeisureHoursTaskTask<TResult>,所以它包含 TResult 類型的 Result 屬性。 在本例中,TResult 代表整數類型。 當 await 套用至 getLeisureHoursTask 時,await 運算式評估為 getLeisureHoursTaskResult 屬性的內容。 值會指派給 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>

非同步方法可以傳回任何具有可存取 GetAwaiter 方法的類型,該方法會傳回「awaiter 類型」的執行個體。 此外,從 GetAwaiter 方法傳回的類型必須具有 System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 屬性。 您可以在編譯器所讀取的屬性一文中,或是工作類型建立器模式的 C# 規格中深入了解。

這項功能是對可等候運算式的補充,描述 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> 類型,這些類型已可滿足非同步程式碼的大部分使用情節。

在 C# 10 和更新版本中,您可以將 AsyncMethodBuilder 屬性套用至非同步方法 (而不是非同步傳回型別宣告),藉此覆寫該類型的建立器。 通常您會套用這個屬性以使用 .NET Runtime 中提供的不同建立器。

使用 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 陳述式列舉每個字組。 方法會在需要從來源字串非同步讀取下一行時等候。

另請參閱