異常過濾器
異常過濾器使開發人員能夠將一個條件(以 boolean
表示式的形式)新增到 catch塊,允許 catch
僅在條件計算為 true
時執行。
異常過濾器允許在原始異常中傳播除錯資訊,其中在 catch
塊內使用 if
語句並重新丟擲異常會停止在原始異常中傳播除錯資訊。使用異常過濾器時,異常將繼續在呼叫堆疊中向上傳播,除非滿足條件。因此,異常過濾器使除錯體驗變得更加容易。偵錯程式不會停止在 throw
語句上,而是停止丟擲異常的語句,並保留當前狀態和所有區域性變數。崩潰轉儲以類似的方式受到影響。
CLR 從一開始就支援異常過濾器,並且通過暴露 CLR 的異常處理模型的一部分,它們可以從 VB.NET 和 F#訪問十多年。只有在 C#6.0 釋出之後,C#開發人員才能使用該功能。
使用異常過濾器
通過將 when
子句附加到 catch
表示式來使用異常過濾器。可以使用在 when
子句中返回 bool
的任何表示式( 等待除外 )。宣告的異常變數 ex
可以從 when
子句中訪問:
var SqlErrorToIgnore = 123;
try
{
DoSQLOperations();
}
catch (SqlException ex) when (ex.Number != SqlErrorToIgnore)
{
throw new Exception("An error occurred accessing the database", ex);
}
可以組合具有 when
子句的多個 catch
塊。返回 true
的第一個 when
子句將導致異常被捕獲。它的 catch
塊將被輸入,而其他 catch
塊將被忽略(它們的 when
條款將不被評估)。例如:
try
{ ... }
catch (Exception ex) when (someCondition) //If someCondition evaluates to true,
//the rest of the catches are ignored.
{ ... }
catch (NotImplementedException ex) when (someMethod()) //someMethod() will only run if
//someCondition evaluates to false
{ ... }
catch(Exception ex) // If both when clauses evaluate to false
{ ... }
條款危險時
警告
使用異常過濾器可能有風險:當從
when
子句中丟擲Exception
時,when
子句中的Exception
將被忽略並被視為false
。這種方法允許開發人員在不處理無效情況的情況下編寫when
子句。
以下示例說明了這種情況:
public static void Main()
{
int a = 7;
int b = 0;
try
{
DoSomethingThatMightFail();
}
catch (Exception ex) when (a / b == 0)
{
// This block is never reached because a / b throws an ignored
// DivideByZeroException which is treated as false.
}
catch (Exception ex)
{
// This block is reached since the DivideByZeroException in the
// previous when clause is ignored.
}
}
public static void DoSomethingThatMightFail()
{
// This will always throw an ArgumentNullException.
Type.GetType(null);
}
請注意,當失敗程式碼在同一函式內時,異常過濾器可避免與使用 throw
相關的令人困惑的行號問題。例如,在這種情況下,行號報告為 6 而不是 3:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) {
6. throw;
7. }
異常行號報告為 6,因為錯誤是在第 6 行的 throw
語句中捕獲並重新丟擲的。
異常過濾器不會發生同樣的情況:
1. int a = 0, b = 0;
2. try {
3. int c = a / b;
4. }
5. catch (DivideByZeroException) when (a != 0) {
6. throw;
7. }
在此示例中,a
為 0,然後忽略 catch
子句,但報告 3 為行號。這是因為它們不會展開堆疊。更具體地說,異常沒有在第 5 行捕獲,因為 a
實際上確實等於 0
,因此沒有機會在第 6 行重新丟擲異常,因為第 6 行不執行。
記錄為副作用
條件中的方法呼叫可能會導致副作用,因此可以使用異常過濾器在異常上執行程式碼而不捕獲它們。利用這個的一個常見例子是 Log
方法總是返回 false
。這允許在除錯時跟蹤日誌資訊,而無需重新丟擲異常。
**請注意,**雖然這似乎是一種舒適的日誌記錄方式,但它可能存在風險,尤其是在使用第三方日誌記錄程式集時。這些可能會在記錄可能無法輕易檢測到的非顯而易見的情況時丟擲異常(請參閱上面的 Risky
when(...)
子句 )。
try
{
DoSomethingThatMightFail(s);
}
catch (Exception ex) when (Log(ex, "An error occurred"))
{
// This catch block will never be reached
}
// ...
static bool Log(Exception ex, string message, params object[] args)
{
Debug.Print(message, args);
return false;
}
以前版本的 C#中的常見方法是記錄並重新丟擲異常。
Version < 6
try
{
DoSomethingThatMightFail(s);
}
catch (Exception ex)
{
Log(ex, "An error occurred");
throw;
}
// ...
static void Log(Exception ex, string message, params object[] args)
{
Debug.Print(message, args);
}
finally
區塊
該 finally
塊執行每次是否引發異常或沒有時間。在 when
中使用表示式的一個微妙之處是異常過濾器在進入內部 finally
塊之前在堆疊中進一步執行。當程式碼嘗試修改全域性狀態(如當前執行緒的使用者或文化)並將其設定回 finally
塊時,這可能會導致意外的結果和行為。
示例:finally
block
private static bool Flag = false;
static void Main(string[] args)
{
Console.WriteLine("Start");
try
{
SomeOperation();
}
catch (Exception) when (EvaluatesTo())
{
Console.WriteLine("Catch");
}
finally
{
Console.WriteLine("Outer Finally");
}
}
private static bool EvaluatesTo()
{
Console.WriteLine($"EvaluatesTo: {Flag}");
return true;
}
private static void SomeOperation()
{
try
{
Flag = true;
throw new Exception("Boom");
}
finally
{
Flag = false;
Console.WriteLine("Inner Finally");
}
}
輸出:
開始
評估:真正的
內在終於最終
抓住
外面
在上面的例子中,如果方法 SomeOperation
不希望洩漏全域性狀態改變為呼叫者的 when
子句,它還應該包含 catch
塊來修改狀態。例如:
private static void SomeOperation()
{
try
{
Flag = true;
throw new Exception("Boom");
}
catch
{
Flag = false;
throw;
}
finally
{
Flag = false;
Console.WriteLine("Inner Finally");
}
}
通常看到 IDisposable
輔助類利用使用塊的語義來實現相同的目標,因為在 using
塊內呼叫的異常開始冒泡堆疊之前,將始終呼叫 IDisposable.Dispose
。