Effective C# 原则44:创建应用程序特定的异常类

JerryXia 发表于 , 阅读 (750)

异常是一种的报告错误的机制,它可以在远离错误发生的地方进行处理错误。所有关于错误发生的的信息必须包含在异常对象中。在错误发生的过程中,你可能想把底层的错误转化成详细的应用程序错误,而且不丢失关于错误的任何信息。你须要仔细考虑关于如何在C#应用程序中创建特殊的异常类。第一步就是要理解什么时候以及为什么要创建新的异常类,以及如何构造继承的异常信息。当开发者使用你的库来写catch语句时,他们是基于特殊的进行时异常在区别为同的行为的。每一个不同的异常类可以有不同的处理要完成:

try {
  Foo( );
  Bar( );
} catch( MyFirstApplicationException e1 )
{
  FixProblem( e1 );
} catch( AnotherApplicationException e2 )
{
  ReportErrorAndContinue( e2 );
} catch( YetAnotherApplicationException e3 )
{
  ReportErrorAndShutdown( e3 );
} catch( Exception e )
{
  ReportGenericError( e );
}
finally
{
  CleanupResources( );
}

不同的catch语句可以因为不同的运行时异常而存在。你,做为库的作者,当异常的catch语句要处理不同的事情时,必须创建或者使用不同的异常类。如果不这样,你的用户就只有唯一一个无聊的选择。在任何一个异常抛出时,你可以挂起或者中止应用程序。这当然是最少的工作,但样是不可能从用户那里赢得声望的。或者,他们 可以取得异常,然后试着断定这个错误是否可以修正:

try {
  Foo( );
  Bar( );
} catch( Exception e )
{
  switch( e.TargetSite.Name )
  {
    case "Foo":
      FixProblem( e );
      break;
    case "Bar":
      ReportErrorAndContinue( e );
      break;
    // some routine called by Foo or Bar:
    default:
      ReportErrorAndShutdown( e );
      break;
  }
} finally
{
  CleanupResources( );
}

这远不及使用多个catch语句有吸引力,这是很脆弱的代码:如果只是常规的修改了名字,它就被破坏了。如果你移动了造成错误的函数调用,放到了一个共享的工具函数中,它也被破坏了。在更深一层的堆栈上发生异常,就会使这样的结构变得更脆弱。

在深入讨论这一话题前,让我先附带说明两个不能做承诺的事情。首先,异常并不能处理你所遇到的所有异常。这并不是一个稳固指导方法,但我喜欢为错误条件抛出异常,这些错误条件如果不立即处理或者报告,可能会在后期产生更严重的问题。例如,数据库里的数据完整性的错误,就应该生产一个异常。这个问题如果忽略就只会越发严重。而像在写入用户的窗口位置失败时,不太像是在后来会产生一系列的问题。返回一个错误代码来指示失败就足够了。

其次,写一个抛出(throw)语句并不意味会在这个时间创建一个新的异常类。我推荐创建更多的异常,而不是只有少数几个常规的自然异常:很从人好像在抛出异常时只对System.Exception情有独钟。可惜只只能提供最小的帮助信息来处理调用代码。相反,考虑创建一些必须的异常类,可以让调用代码明白是什么情况,而且提供了最好的机会来恢复它。

再说一遍:实际上要创建不同的异常类的原则,而且唯一原因是让你的用户在写catch语句来处理错误时更简单。查看分析这些错误条件,看哪些可以放一类里,成为一个可以恢复错误的行为,然后创建指定的异常类来处理这些行为。你的应用程序可以从一个文件或者目录丢失的错误中恢复过来吗?它还可以从安全权限不足的情况下恢复吗?网络资源丢失又会怎样呢?对于这种遇到不同的错误,可能要采取不同的恢复机制时,你应该为不同的行为创建新的异常类。

因此,现在你应该创建你自己的异常类了。当你创建一个异常类时,你有很多责任要完成。你应该总是从System.ApplicationException类派生你的异常类,而不是System.Exception类。对于这个基类你不用添加太多的功能。对于不同的异常类,它已经具有可以在不同的catch语句中处理的能力了。

但也不要从异常类中删除任何东西。ApplicationException 类有四个不同的构造函数:

// Default constructor
public ApplicationException( );

// Create with a message.
public ApplicationException( string );

// Create with a message and an inner exception.
public ApplicationException( string, Exception );

// Create from an input stream.
protected ApplicationException(
  SerializationInfo, StreamingContext );

当你创建一个新的异常类时,你应该创建这个四构造函数。不同的情况调用不同的构造方法来构造异常。你可以委托这个工作给基类来实现:

public class MyAssemblyException :
  ApplicationException
{
  public MyAssemblyException( ) :
    base( )
  {
  }

  public MyAssemblyException( string s ) :
    base( s )
  {
  }

  public MyAssemblyException( string s,
    Exception e) :
    base( s, e )
  {
  }

  protected MyAssemblyException(
    SerializationInfo info, StreamingContext cxt ) :
    base( info, cxt )
  {
  }
}

构造函数须要的这个异常参数值得讨论一下。有时候,你所使用的类库之一会发生异常。调用你的库的代码可能会取得最小的关于可能修正行为的信息,当你简单从你使用的异常上传参数时:

public double DoSomeWork( )
{
  // This might throw an exception defined
  // in the third party library:
  return ThirdPartyLibrary.ImportantRoutine( );
}

当你创建异常时,你应该提供你自己的库信息。抛出你自己详细的异常,以及包含源异常做为它的内部异常属性。你可以提供你所能提供的最多的额外信息:

public double DoSomeWork( )
{
  try {
    // This might throw an exception defined
    // in the third party library:
    return ThirdPartyLibrary.ImportantRoutine( );
  } catch( Exception e )
    {
      string msg =
        string.Format("Problem with {0} using library",
          this.ToString( ));
      throw new DoingSomeWorkException( msg, e );
    }
  }
}

这个新的版本会在问题发生的地方创建更多的信息。当你已经创建了一个恰当的ToString()方法时(参见原则5),你就已经创建了一个可以完整描述问题发生的异常对象。更多的,一个内联异常显示了产生问题的根源:也就是你所使用的第三方库里的一些信息。

这一技术叫做异常转化,转化一个底的层异常到更高级的异常,这样可以提供更多的关于错误的内容。你越是创建多的关于错误的额外的信息,就越是容易让它用于诊断,以及可能修正错误。通过创建你自己的异常类,你可能转化底层的问题到详细的异常,该异常包含所详细的应用程序信息,这可以帮助你诊断程序以及尽可能的修正问题。

希望你的应用程序不是经常抛出异常,但它会发生。如果你不做任何详细的处理,你的应用程序可能会产生默认的.Net框架异常,而不管是什么错误在你调用的方法里发生。提供更详细的信息将会让你以及你的用户,在实际应用中诊断程序以及可能的修正错误大有帮助。当且仅当对于错误有不同的行为要处理时,你才应该创建不同的异常类。你可以通过提供所有基类支持的构造函数,来创建全功能的异常类。你还可以使用InnerException属性来承载底层错误条件的所有错误信息。

添加新评论