C#创建自定义异常

示例

允许您实现可以像其他任何异常一样引发的自定义异常。当您希望在运行时将异常与其他错误区分开来时,这很有意义。

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

创建自定义异常类

要创建自定义异常,请创建以下子类Exception:

public class ParserException : Exception
{
    public ParserException() : 
      base("解析出错,我们没有其他信息。") { }
}

当您想向捕获程序提供其他信息时,自定义异常变得非常有用:

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;}    
}

现在,当您catch(ParserException x)拥有其他语义来微调异常处理时。

自定义类可以实现以下功能以支持其他方案。

重新投掷

在解析过程中,仍然需要关注原始异常。在此示例中,这是FormatException因为代码尝试解析一个字符串,该字符串应为数字。在这种情况下,自定义异常应支持包含“ InnerException ”:

//新的构造函数:
ParserException(string msg, Exception inner) : base(msg, inner) {
}

序列化

在某些情况下,您的例外可能必须跨越AppDomain边界。如果您的解析器在其自己的AppDomain中运行以支持新解析器配置的热重载,就会出现这种情况。在Visual Studio中,您可以使用Exception模板来生成这样的代码。

[Serializable]
public class ParserException : Exception
{
    // 没有参数的构造函数允许在没有参数的情况下抛出异常
    //提供任何信息,包括错误消息。应该包括在内
    //如果您的例外情况有意义但没有其他详细信息。应该
    // 通过调用基本构造函数设置消息(默认消息没有帮助)。
    public ParserException()
        : base("解析器失败。")
    {}

    // 带message参数的构造方法允许覆盖默认错误消息。
    // 如果用户可以提供比以下内容更多的有用信息,则应包括在内
    // 通用自动生成的消息。
    public ParserException(string message) 
        : base(message)
    {}

    //序列化支持的构造函数。如果您的例外包含自定义
    // 属性,请在此处阅读其值。
    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)
{
    //假设您添加了此构造函数
    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);
}