创建自定义异常

你可以实现可以像任何其他异常一样抛出的自定义异常。当你希望在运行时期间将异常与其他错误区分开来时,这是有意义的。

在此示例中,我们将创建一个自定义异常,以便在解析复杂输入时清楚地处理应用程序可能遇到的问题。

创建自定义异常类

要创建自定义异常,请创建 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);
}