例外狀況處理陳述式 -
您可以使用 throw 和 try 陳述式來處理例外狀況。 使用 throw 陳述式 來擲回例外狀況。 使用 try 陳述式 來攔截並處理程式碼區塊執行期間可能發生的例外狀況。
throw 陳述式
throw 陳述式會擲回例外狀況:
if (shapeAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}
在 throw e; 陳述式中,運算式 e 的結果必須隱含轉換成 System.Exception。
您可以使用內建例外狀況類別,例如 ArgumentOutOfRangeException 或 InvalidOperationException。 .NET 也提供下列協助程式方法,以在特定情況下擲回例外狀況:ArgumentNullException.ThrowIfNull 和 ArgumentException.ThrowIfNullOrEmpty。 您也可以定義衍生自 System.Exception 的自有例外狀況類別。 如需詳細資訊,請參閱建立和擲回例外狀況。
在 catch 區塊內,您可以使用 throw; 陳述式來重新擲回 catch 區塊所處理的例外狀況:
try
{
ProcessShapes(shapeAmount);
}
catch (Exception e)
{
LogError(e, "Shape processing failed.");
throw;
}
注意
throw; 會保留例外狀況的原始堆疊追蹤,其儲存在 Exception.StackTrace 屬性中。 相反地,throw e; 會更新 StackTrace 的 e 屬性。
擲回例外狀況時,Common Language Runtime (CLR) 會尋找可處理此例外狀況的 catch 區塊。 如果目前執行的方法不包含這類 catch 區塊,CLR 會在呼叫堆疊上查看呼叫目前方法的方法,依此類推。 如果找不到 catch 區塊,CLR 就會終止執行中的執行緒。 如需詳細資訊,請參閱 C# 語言規格的如何處理例外狀況一節。
throw 運算式
您也可以使用 throw 作為運算式。 這在一些案例中可能很方便,包括:
條件運算子。 下列範例會使用
throw運算式,在傳遞的陣列 ArgumentException 空白時擲回args:string first = args.Length >= 1 ? args[0] : throw new ArgumentException("Please supply at least one argument.");Null 聯合運算子。 下列範例會使用
throw運算式,在要指派給屬性的字串為 ArgumentNullException 時擲回null:public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }運算式主體 ambda 或方法。 下列範例會使用
throw運算式來擲回 InvalidCastException,指出不支援轉換成 DateTime 值:DateTime ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Conversion to a DateTime is not supported.");
try 陳述式
您可以使用下列任何形式的 try 陳述式:try-catch - 處理在 try 區塊內執行程式碼時可能發生的例外狀況,try-finally - 指定當控制項離開 try 區塊時執行的程式碼,以及 try-catch-finally - 做為上述兩種形式的組合。
try-catch 陳述式
使用 try-catch 陳述式 來處理程式碼區塊執行期間可能發生的例外狀況。 將可能發生例外狀況的程式碼放在 try 區塊內。 使用 catch 子句來指定您想要在對應 catch 區塊中處理的例外狀況基底類型:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
您可以提供數個 catch 子句:
try
{
var result = await ProcessAsync(-3, 4, cancellationToken);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Processing is cancelled.");
}
發生例外狀況時,會以指定的順序 (從上到下) 檢查 catch 子句。 最多只會針對任何擲回的例外狀況執行一個 catch 區塊。 如上述範例所示,您可以省略例外狀況變數的宣告,只在 catch 子句中指定例外狀況類型。 不含任何指定例外狀況類型的 catch 子句符合任何例外狀況,如果存在,則必須是最後一個 catch 子句。
如果您想要重新擲回攔截到的例外狀況,請使用 throw 陳述式,如下列範例所示:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
LogError(e, "Processing failed.");
throw;
}
注意
throw; 會保留例外狀況的原始堆疊追蹤,其儲存在 Exception.StackTrace 屬性中。 相反地,throw e; 會更新 StackTrace 的 e 屬性。
when 例外狀況篩選條件
除了例外狀況類型,您也可以指定例外狀況篩選條件,進一步檢查例外狀況,並決定對應的 catch 區塊是否處理該例外狀況。 例外狀況篩選條件是接在 when 關鍵字之後的布林運算式,如下列範例所示:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
上述範例會使用例外狀況篩選來提供單一 catch 區塊,以處理兩個指定類型的例外狀況。
您可以為相同的例外狀況類型提供數個 catch 子句,前提是這些子句是由例外狀況篩選條件區別。 其中一個子句可能沒有任何例外狀況篩選條件。 如果這類子句存在,其必須是指定該例外狀況類型的最後一個子句。
如果 catch 子句具有例外狀況篩選條件,其指定的例外狀況類型可與出現在其後之 catch 子句的例外狀況類型相同或衍生程度較低。 例如,如果例外狀況篩選條件存在,則 catch (Exception e) 子句不需要是最後一個子句。
異常篩選器與傳統異常處理
與傳統的異常處理方法相比,異常過濾器具有顯著的優勢。 主要差異在於評估異常狀況處理邏輯 時 :
-
例外狀況篩選器 (
when):在取消堆疊 之前 評估篩選運算式。 這表示原始呼叫堆疊和所有局部變數在篩選評估期間保持不變。 -
傳統
catch區塊:catch 區塊在堆疊展開 後 執行,可能會遺失有價值的偵錯資訊。
以下是顯示差異的比較:
public static void DemonstrateStackUnwindingDifference()
{
var localVariable = "Important debugging info";
try
{
ProcessWithExceptionFilter(localVariable);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
{
// Exception filter: Stack not unwound yet.
// localVariable is still accessible in debugger.
// Call stack shows original throwing location.
Console.WriteLine($"Caught with filter: {ex.Message}");
Console.WriteLine($"Local variable accessible: {localVariable}");
}
try
{
ProcessWithTraditionalCatch(localVariable);
}
catch (InvalidOperationException ex)
{
// Traditional catch: Stack already unwound.
// Some debugging information may be lost.
if (ex.Message.Contains("traditional"))
{
Console.WriteLine($"Caught with if: {ex.Message}");
Console.WriteLine($"Local variable accessible: {localVariable}");
}
else
{
throw; // Re-throws and further modifies stack trace.
}
}
}
private static void ProcessWithExceptionFilter(string context)
{
throw new InvalidOperationException($"Exception for filter demo: {context}");
}
private static void ProcessWithTraditionalCatch(string context)
{
throw new InvalidOperationException($"Exception for traditional demo: {context}");
}
異常篩選器的優點
- 更好的偵錯體驗:由於堆疊在篩選相符之前不會回復,因此偵錯工具可以顯示原始失敗點,且所有區域變數都完好無損。
- 效能優勢:如果沒有相符的篩選條件,例外狀況會繼續傳播,而不會產生堆疊回想和還原的額外負荷。
- 更乾淨的程式碼:多個篩選器可以處理相同異常類型的不同條件,而不需要巢狀的 if-else 陳述式。
- 記錄和診斷:在決定是否處理異常之前,您可以檢查並記錄異常詳細資料:
public static void DemonstrateDebuggingAdvantage()
{
var contextData = new Dictionary<string, object>
{
["RequestId"] = Guid.NewGuid(),
["UserId"] = "user123",
["Timestamp"] = DateTime.Now
};
try
{
// Simulate a deep call stack.
Level1Method(contextData);
}
catch (Exception ex) when (LogAndFilter(ex, contextData))
{
// This catch block may never execute if LogAndFilter returns false.
// But LogAndFilter can examine the exception while the stack is intact.
Console.WriteLine("Exception handled after logging");
}
}
private static void Level1Method(Dictionary<string, object> context)
{
Level2Method(context);
}
private static void Level2Method(Dictionary<string, object> context)
{
Level3Method(context);
}
private static void Level3Method(Dictionary<string, object> context)
{
throw new InvalidOperationException("Error in deep call stack");
}
private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
// This method runs before stack unwinding.
// Full call stack and local variables are still available.
Console.WriteLine($"Exception occurred: {ex.Message}");
Console.WriteLine($"Request ID: {context["RequestId"]}");
Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
// Return true to handle the exception, false to continue search.
return ex.Message.Contains("deep call stack");
}
何時使用例外狀況篩選
當您需要時,請使用例外狀況篩選:
- 根據特定條件或屬性處理異常。
- 保留原始呼叫堆疊以進行偵錯。
- 在決定是否處理例外狀況之前,先記錄或檢查例外狀況。
- 根據內容以不同的方式處理相同的例外類型。
public static void HandleFileOperations(string filePath)
{
try
{
// Simulate file operation that might fail.
ProcessFile(filePath);
}
catch (IOException ex) when (ex.Message.Contains("access denied"))
{
Console.WriteLine("File access denied. Check permissions.");
}
catch (IOException ex) when (ex.Message.Contains("not found"))
{
Console.WriteLine("File not found. Verify the path.");
}
catch (IOException ex) when (IsNetworkPath(filePath))
{
Console.WriteLine($"Network file operation failed: {ex.Message}");
}
catch (IOException)
{
Console.WriteLine("Other I/O error occurred.");
}
}
private static void ProcessFile(string filePath)
{
// Simulate different types of file exceptions.
if (filePath.Contains("denied"))
throw new IOException("File access denied");
if (filePath.Contains("missing"))
throw new IOException("File not found");
if (IsNetworkPath(filePath))
throw new IOException("Network timeout occurred");
}
private static bool IsNetworkPath(string path)
{
return path.StartsWith(@"\\") || path.StartsWith("http");
}
堆疊追蹤保留
例外狀況篩選會保留原始 ex.StackTrace 屬性。 如果子 catch 句無法處理例外狀況並重新擲回,則原始堆疊資訊會遺失。 篩選器when不會展開堆疊,因此如果篩選器是 when,則false不會變更原始堆疊追蹤。
異常篩選方法在保留偵錯資訊對於診斷問題至關重要的應用程式中非常有價值。
非同步和迭代器方法的例外狀況
如果在非同步函式中發生例外狀況,當您 await 函式的結果時,它會傳播至函式的呼叫端,如下列範例所示:
public static async Task Run()
{
try
{
Task<int> processing = ProcessAsync(-1);
Console.WriteLine("Launched processing.");
int result = await processing;
Console.WriteLine($"Result: {result}.");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
// Output:
// Launched processing.
// Processing failed: Input must be non-negative. (Parameter 'input')
}
private static async Task<int> ProcessAsync(int input)
{
if (input < 0)
{
throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
}
await Task.Delay(500);
return input;
}
如果迭代器方法發生例外狀況,則只有在迭代器進到下一個元素時,才會傳播至呼叫端。
try-finally 陳述式
在 try-finally 陳述式中,當控制項離開 finally 區塊時,就會執行 try 區塊。 控制項可能會由於下列事項而離開 try 區塊:
- 正常執行,
- 執行 跳躍陳述式 (也就是
return、break、continue或goto),或 - 傳播
try區塊以外的例外狀況。
下列範例會使用 finally 區塊,在控制項離開方法之前重設物件的狀態:
public async Task HandleRequest(int itemId, CancellationToken ct)
{
Busy = true;
try
{
await ProcessAsync(itemId, ct);
}
finally
{
Busy = false;
}
}
您也可以使用 finally 區塊來清除 try 區塊中使用的配置資源。
注意
當資源的類型實作 IDisposable 或 IAsyncDisposable 介面時,請考慮 using 陳述式。
using 陳述式可確保當控制項離開 using 陳述式時,會處置已取得的資源。 編譯器會將 using 陳述式轉換成 try-finally 陳述式。
執行 finally 區塊取決於作業系統是否選擇觸發例外狀況回溯作業。 未執行 finally 區塊的唯一情況涉及立即終止程式。 例如,這類終止可能會因為 Environment.FailFast 呼叫或 OverflowException 或 InvalidProgramException 例外狀況而發生。 大部分的作業系統會在停止和卸載流程中執行合理的資源清除。
try-catch-finally 陳述式
您可以使用 try-catch-finally 陳述式來處理 try 區塊執行期間可能發生的例外狀況,並指定當控制項離開 try 陳述式時必須執行的程式碼:
public async Task ProcessRequest(int itemId, CancellationToken ct)
{
Busy = true;
try
{
await ProcessAsync(itemId, ct);
}
catch (Exception e) when (e is not OperationCanceledException)
{
LogError(e, $"Failed to process request for item ID {itemId}.");
throw;
}
finally
{
Busy = false;
}
}
當 catch 區塊處理例外狀況時,finally 區塊會在執行該 catch 區塊之後執行 (即使執行 catch 區塊期間發生另一個例外狀況也一樣)。 如需 catch 和 finally 區塊的相關資訊,請分別參閱try-catch 陳述式和 try-finally 陳述式章節。
C# 語言規格
如需詳細資訊,請參閱 C# 語言規格的下列幾節: