建立自定義異常

你可以實現可以像任何其他異常一樣丟擲的自定義異常。當你希望在執行時期間將異常與其他錯誤區分開來時,這是有意義的。

在此示例中,我們將建立一個自定義異常,以便在解析複雜輸入時清楚地處理應用程式可能遇到的問題。

建立自定義異常類

要建立自定義異常,請建立 Exception 的子類:

public class ParserException : Exception
{
    public ParserException() : 
      base("The parsing went wrong and we have no additional information.") { }
}

當你想要向捕手提供其他資訊時,自定義異常變得非常有用:

public class ParserException : Exception
{
    public ParserException(string fileName, int lineNumber) : 
      base($"Parser error in {fileName}:{lineNumber}") 
    {
      FileName = fileName;
      LineNumber = lineNumber;
    }
    public string FileName {get; private set;}
    public int LineNumber {get; private set;}    
}

現在,當你使用時,你將擁有額外的語義來微調異常處理。

自定義類可以實現以下功能以支援其他方案。

重新投擲

在解析過程中,原始異常仍然很有用。在這個例子中,它是一個 FormatException,因為程式碼試圖解析一段字串,這個字串應該是一個數字。在這種情況下,自定義異常應該支援包含’ InnerException ‘:

//new constructor:
ParserException(string msg, Exception inner) : base(msg, inner) {
}

系列化

在某些情況下,你的異常可能必須跨越 AppDomain 邊界。如果你的解析器在其自己的 AppDomain 中執行以支援熱重新載入新的解析器配置,則會出現這種情況。在 Visual Studio 中,你可以使用 Exception 模板生成這樣的程式碼。

[Serializable]
public class ParserException : Exception
{
    // Constructor without arguments allows throwing your exception without
    // providing any information, including error message. Should be included
    // if your exception is meaningful without any additional details. Should
    // set message by calling base constructor (default message is not helpful).
    public ParserException()
        : base("Parser failure.")
    {}

    // Constructor with message argument allows overriding default error message.
    // Should be included if users can provide more helpful messages than
    // generic automatically generated messages.
    public ParserException(string message) 
        : base(message)
    {}

    // Constructor for serialization support. If your exception contains custom
    // properties, read their values here.
    protected ParserException(SerializationInfo info, StreamingContext context) 
        : base(info, context)
    {}
}

使用 ParserException

try
{
    Process.StartRun(fileName)
}
catch (ParserException ex)
{
    Console.WriteLine($"{ex.Message} in ${ex.FileName}:${ex.LineNumber}");
}
catch (PostProcessException x) 
{
    ...
}

你還可以使用自定義異常來捕獲和包裝異常。這樣,許多不同的錯誤可以轉換為對應用程式更有用的單個錯誤型別:

try
{
    int foo = int.Parse(token);
}
catch (FormatException ex)
{
    //Assuming you added this constructor
    throw new ParserException(
      $"Failed to read {token} as number.", 
      FileName, 
      LineNumber, 
      ex);
}

通過提出自己的自定義異常來處理異常時,通常應該在 InnerException 屬性中包含對原始異常的引用,如上所示。

安全問題

如果通過允許使用者檢視應用程式的內部工作原理來暴露異常的原因可能會破壞安全性,那麼包裝內部異常可能是個壞主意。如果你要建立將由其他人使用的類庫,這可能適用。

以下是如何在不包裝內部異常的情況下引發自定義異常的方法:

try
{
  // ...
}
catch (SomeStandardException ex)
{
  // ...
  throw new MyCustomException(someMessage);
}

結論

在引發自定義異常時(使用換行或使用未解包的新異常),應該引發對呼叫者有意義的異常。例如,類庫的使用者可能不太瞭解該庫如何執行其內部工作。類庫的依賴項引發的異常沒有意義。相反,使用者想要一個與類庫如何以錯誤方式使用這些依賴關係相關的異常。

try
{
  // ...
}
catch (IOException ex)
{
  // ...
  throw new StorageServiceException(@"The Storage Service encountered a problem saving
your data. Please consult the inner exception for technical details. 
If you are not able to resolve the problem, please call 555-555-1234 for technical       
assistance.", ex);
}