異常過濾器

異常過濾器使開發人員能夠將一個條件(以 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