非同步方法的傳回型別 (C#)
非同步方法可有下列傳回型別:
- 執行作業但不傳回任何值的非同步方法為 Task。
- 傳回值的非同步方法為 Task<TResult>。
- 處理常式為
void
。 - 任何具有可存取
GetAwaiter
方法的類型。GetAwaiter
方法傳回的物件必須實作 System.Runtime.CompilerServices.ICriticalNotifyCompletion 介面。 - IAsyncEnumerable<T>,適用於傳回「非同步資料流」的非同步方法。
如需非同步方法的詳細資訊,請參閱使用 async 和 await 進行非同步程式設計 (C#)。
Windows 工作負載也存在幾種其他類型:
- DispatcherOperation,適用於僅限 Windows 的非同步作業。
- IAsyncAction,適用於 UWP 中不會傳回值的非同步動作。
- IAsyncActionWithProgress<TProgress>,適用於 UWP 中報告進度但不會傳回值的非同步動作。
- IAsyncOperation<TResult>,適用於 UWP 中傳回值的非同步作業。
- IAsyncOperationWithProgress<TResult,TProgress>,適用於 UWP 中報告進度並傳回值的非同步作業。
工作傳回型別
不包含 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
的應用區分開來,深入了解 await
從 Task<T>
擷取結果的運作方式,如下列程式碼所示。 呼叫不會立即等候的 GetLeisureHoursAsync
方法,會傳回 Task<int>
,如您所預期的方法宣告。 在範例中,工作會指派給 getLeisureHoursTask
變數。 因為 getLeisureHoursTask
是 Task<TResult>,所以它包含 TResult
類型的 Result 屬性。 在本例中,TResult
代表整數類型。 當 await
套用至 getLeisureHoursTask
時,await 運算式評估為 getLeisureHoursTask
之 Result 屬性的內容。 值會指派給 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 的非同步方法呼叫端無法攔截方法擲回的例外狀況。 這類未處理的例外狀況可能會導致您的應用程式失敗。 如果方法傳回 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
方法的類型,該方法會傳回「awaiter 類型」的執行個體。 此外,從 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>
類型,這些類型已可滿足非同步程式碼的大部分使用情節。
在 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
陳述式列舉每個字組。 方法會在需要從來源字串非同步讀取下一行時等候。