當作預設使用 await 。
await 它讓你有自然的例外流程,保持程式碼可讀性,並避免同步與非同步的死鎖。
有時候仍然需要封鎖 Task,例如在傳統的同步進入點。 在這種情況下,你需要了解每個 API 如何呈現例外。
比較阻擋 API 的例外傳播
當你必須封鎖任務時,請使用 GetAwaiter()。GetResult() 以保留原始例外類型:
public static class SingleExceptionExample
{
public static Task<int> FaultAsync()
{
return Task.FromException<int>(new InvalidOperationException("Single failure"));
}
public static void ShowBlockingDifferences()
{
try
{
_ = FaultAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}");
}
}
}
Public Module SingleExceptionExample
Public Function FaultAsync() As Task(Of Integer)
Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
End Function
Public Sub ShowBlockingDifferences()
Try
Dim ignored = FaultAsync().GetAwaiter().GetResult()
Catch ex As Exception
Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}")
End Try
End Sub
End Module
Task<TResult>.Result 以及 Wait 將例外包裹在 AggregateException中,這使得例外處理變得複雜。 以下程式碼使用這些 API,卻接收錯誤的例外類型:
// ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
public static class SingleExceptionBadExample
{
public static Task<int> FaultAsync()
{
return Task.FromException<int>(new InvalidOperationException("Single failure"));
}
public static void ShowBlockingDifferences()
{
try
{
_ = FaultAsync().Result;
}
catch (AggregateException ex)
{
Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}");
}
}
}
' ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
Public Module SingleExceptionBadExample
Public Function FaultAsync() As Task(Of Integer)
Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
End Function
Public Sub ShowBlockingDifferences()
Try
Dim ignored = FaultAsync().Result
Catch ex As AggregateException
Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}")
End Try
End Sub
End Module
對於有多個異常的任務,GetAwaiter().GetResult() 仍會拋出一個異常,但 Task.Exception 會使用 AggregateException 來儲存包含所有內部異常的異常集合:
public static class MultiExceptionExample
{
public static async Task FaultAfterDelayAsync(string name, int milliseconds)
{
await Task.Delay(milliseconds);
throw new InvalidOperationException($"{name} failed");
}
public static void ShowMultipleExceptions()
{
Task combined = Task.WhenAll(
FaultAfterDelayAsync("First", 10),
FaultAfterDelayAsync("Second", 20));
try
{
combined.GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}");
}
if (combined.IsFaulted && combined.Exception is not null)
{
AggregateException allErrors = combined.Exception.Flatten();
Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.");
}
else
{
Console.WriteLine("Task.Exception is null because the task didn't fault.");
}
}
}
Public Module MultiExceptionExample
Public Async Function FaultAfterDelayAsync(name As String, milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
Throw New InvalidOperationException($"{name} failed")
End Function
Public Sub ShowMultipleExceptions()
Dim combined As Task = Task.WhenAll(
FaultAfterDelayAsync("First", 10),
FaultAfterDelayAsync("Second", 20))
Try
combined.GetAwaiter().GetResult()
Catch ex As Exception
Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}")
End Try
If combined.IsFaulted AndAlso combined.Exception IsNot Nothing Then
Dim allErrors As AggregateException = combined.Exception.Flatten()
Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.")
Else
Console.WriteLine("Task.Exception was not available because the task did not fault.")
End If
End Sub
End Module
Task.Result 與 GetAwaiter().GetResult()
在兩個 API 之間做選擇時,請參考以下指引:
- 可以的話,盡量使用
await。 這樣可以避免阻塞和死結風險。 - 如果你必須封鎖且想要原始例外類型,請使用
GetAwaiter().GetResult()。 在 WinForms 應用程式中,請注意事件處理程序文章中常見 的陷阱與死結 章節。 - 如果你現有的程式碼期望 AggregateException,請使用
Result或Wait()並檢查InnerExceptions。
這些規則僅影響例外形狀。 兩個 API 仍然會阻擋目前的執行緒,因此在單執行緒環境中都有可能發生死鎖。 要了解如何正確完成所有程式碼路徑上的任務,請參閱 完成你的任務。
現代 .NET 中未被觀察到的任務例外
當錯誤的 TaskScheduler.UnobservedTaskException 在例外未被程式碼觀察到前就被終結時,運行時會引發 Task。
在現代 .NET 中,未被觀察到的例外不再預設讓程序當機。 執行環境會在運行過程中透過事件回報狀態,然後繼續執行。
public static class UnobservedTaskExceptionExample
{
public static void ShowEventBehavior()
{
bool eventRaised = false;
TaskScheduler.UnobservedTaskException += (_, args) =>
{
eventRaised = true;
Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).");
args.SetObserved();
};
_ = Task.Run(() => throw new ApplicationException("Background failure"));
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine(eventRaised
? "Event was raised. The process continued."
: "Event was not observed in this short run. The process still continued.");
}
}
Public Module UnobservedTaskExceptionExample
Public Sub ShowEventBehavior()
Dim eventRaised As Boolean = False
AddHandler TaskScheduler.UnobservedTaskException,
Sub(sender, args)
eventRaised = True
Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).")
args.SetObserved()
End Sub
Task.Run(Sub() Throw New ApplicationException("Background failure"))
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
If eventRaised Then
Console.WriteLine("Event was raised. The process continued.")
Else
Console.WriteLine("Event was not observed in this short run. The process still continued.")
End If
End Sub
End Module
使用事件進行診斷和遙測。 不要用這個事件來取代非同步流程中正常的例外處理。