Effective C# 原則44:建立應用程式特定的異常類(譯)

來源:互聯網
上載者:User

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.
 
   

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.