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.