自定义异常类(转)
Effective C# 原则44:创建应用程序特定的异常类
Item 44: Create Complete Application-Specific Exception Classes
异常是一种的报告错误的机制,它可以在远离错误发生的地方进行处理错误。所有关于错误发生的的信息必须包含在异常对象中。在错误发生的过程中,你可 能想把底层的错误转化成详细的应用程序错误,而且不丢失关于错误的任何信息。你须要仔细考虑关于如何在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属性来承载底层错误条
件的所有错误信息。
=====================================
Item 44: Create Complete Application-Specific Exception Classes
Exceptions
are the mechanism of reporting errors that might be handled at a
location far removed from the location where the error occurred. All
the information about the error's cause must be contained in the
exception object. Along the way, you might want to translate a
low-level error to more of an application-specific error, without
losing any information about the original error. You need to be very
thoughtful about when you create your own specific exception classes in
your C# applications.
The first step is to understand when and why to create new exception classes, and how to construct informative exception hierarchies. When developers using your libraries write catch clauses, they differentiate actions based on the specific runtime type of the exception. Each different exception class can have a different set of actions taken:
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( );
}
Different catch clauses can exist for different runtime types of exceptions. You, as a library author, must create or use different exception classes when catch clauses might take different actions. If you don't, your users are left with only unappealing options. You can punt and terminate the application whenever an exception gets thrown. That's certainly less work, but it won't win kudos from users. Or, they can reach into the exception to try to determine whether the error can be corrected:
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( );
}
That's far less appealing than using multiple catch clauses. It's very brittle code: If you change the name of a routine, it's broken. If you move the error-generating calls into a shared utility function, it's broken. The deeper into the call stack that an exception is generated, the more fragile this kind of construct becomes.
Before going any deeper into this topic, let me add two disclaimers. First, exceptions are not for every error condition you encounter. There are no firm guidelines, but I prefer throwing exceptions for error conditions that cause long-lasting problems if they are not handled or reported immediately. For example, data integrity errors in a database should generate an exception. The problem only gets bigger if it is ignored. Failure to correctly write the user's window location preferences is not likely to cause far-reaching consequences. A return code indicating the failure is sufficient.
Second, writing a tHRow statement does not mean it's time to create a new exception class. My recommendation on creating more rather than fewer Exception classes comes from normal human nature: People seem to gravitate to overusing System.Exception anytime they throw an exception. That provides the least amount of helpful information to the calling code. Instead, think through and create the necessary exceptions classes to enable calling code to understand the cause and provide the best chance of recovery.
I'll say it again: The reason for different exception classesin fact, the only reasonis to make it easier to take different actions when your users write catch handlers. Look for those error conditions that might be candidates for some kind of recovery action, and create specific exception classes to handle those actions. Can your application recover from missing files and directories? Can it recover from inadequate security privileges? What about missing network resources? Create new exception classes when you encounter errors that might lead to different actions and recovery mechanisms.
So now you are creating your own exception classes. You do have very specific responsibilities when you create a new exception class. You should always derive your exception classes from the System.ApplicationException class, not the System.Exception class. You will rarely add capabilities to this base class. The purpose of different exception classes is to have the capability to differentiate the cause of errors in catch clauses.
But don't take anything away from the exception classes you create, either. The ApplicationException class contains four constructors:
// 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 );
When you create a new exception class, create all four of these constructors. Different situations call for the different methods of constructing exceptions. You delegate the work to the base class implementation:
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 )
{
}
}
The constructors that take an exception parameter deserve a bit more discussion. Sometimes, one of the libraries you use generates an exception. The code that called your library will get minimal information about the possible corrective actions when you simply pass on the exceptions from the utilities you use:
public double DoSomeWork( )
{
// This might throw an exception defined
// in the third party library:
return ThirdPartyLibrary.ImportantRoutine( );
}
You should provide your own library's information when you generate the exception. Throw your own specific exception, and include the original exception as its InnerException property. You can provide as much extra information as you can generate:
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 );
}
}
}
This new version creates more information at the point where the problem is generated. As long as you have created a proper ToString() method (see Item 5), you've created an exception that describes the complete state of the object that generated the problem. More than that, the inner exception shows the root cause of the problem: something in the third-party library you used.
This technique is called exception translation, translating a low-level exception into a more high-level exception that provides more context about the error. The more information you generate when an error occurs, the easier it will be for users to diagnose and possibly correct the error. By creating your own exception types, you can translate low-level generic problems into specific exceptions that contain all the application-specific information that you need to fully diagnose and possibly correct the problem.
Your application will throw exceptionshopefully not often, but it will happen. If you don't do anything specific, your application will generate the default .NET Framework exceptions whenever something goes wrong in the methods you call on the core framework. Providing more detailed information will go a long way to enabling you and your users to diagnose and possibly correct errors in the field. You create different exception classes when different corrective actions are possible and only when different actions are possible. You create full-featured exception classes by providing all the constructors that the base exception class supports. You use the InnerException property to carry along all the error information generated by lower-level error conditions.