程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則4:用條件屬性而不是#if預編譯塊

Effective C#原則4:用條件屬性而不是#if預編譯塊

編輯:關於C#

使用#if/#endif 塊可以在同樣源碼上生成不同的編譯(結果),大多數debug 和release兩個版本。但它們決不是我們喜歡用的工具。由於#if/#endif很容易 被濫用,使得編寫的代碼難於理解且更難於調試。程序語言設計者有責任提供更 好的工具,用於生成在不同運行環境下的機器代碼。C#就提供了條件屬性 (Conditional attribute)來識別哪些方法可以根據環境設置來判斷是否應該被 調用。

(譯注:屬性在C#裡有兩個單詞,一個是property另一個是 attribute,它們有不是的意思,但譯為中文時一般都是譯為了屬性。property 是指一個對象的性質,也就是Item1裡說的屬性。而這裡的attribute指的是.net 為特殊的類,方法或者property附加的屬性。可以在MSDN裡查找attribute取得 更多的幫助,總之要注意:attribute與property的意思是完全不一樣的。)

這個方法比條件編譯#if/#endif更加清晰明白。編譯器可以識別 Conditional屬性,所以當條件屬性被應用時,編譯器可以很出色的完成工作。 條件屬性是在方法上使用的,所以這就使用你必須把不同條件下使用的代碼要寫 到不同的方法裡去。當你要為不同的條件生成不同的代碼時,請使用條件屬性而 不是#if/#endif塊。

很多編程老手都在他們的項目裡用條件編譯來檢測 先決條件(per-conditions)和後續條件(post-conditions)。

(譯注: per-conditions,先決條件,是指必須滿足的條件,才能完成某項工作,而 post-conditions,後續條件,是指完成某項工作後一定會達到的條件。例如某 個函數,把某個對象進行轉化,它要求該對象不能為空,轉化後,該對象一定為 整形,那麼:per-conditions就是該對象不能為空,而post-conditions就是該 對象為整形。例子不好,但可以理解這兩個概念。)

你可能會寫一個私有 方法來檢測所有的類及持久對象。這個方法可能會是一個條件編譯塊,這樣可以 使它只在debug時有效。

private void CheckState( )
{
// The Old way:
#if DEBUG
 Trace.WriteLine( "Entering CheckState for Person" );
 // Grab the name of the calling routine:
 string methodName =
  new StackTrace( ).GetFrame( 1 ).GetMethod( ).Name;
 Debug.Assert( _lastName != null,
  methodName,
  "Last Name cannot be null" );
 Debug.Assert( _lastName.Length > 0,
  methodName,
  "Last Name cannot be blank" );
 Debug.Assert( _firstName != null,
  methodName,
  "First Name cannot be null" );
 Debug.Assert( _firstName.Length > 0,
  methodName,
  "First Name cannot be blank" );
 Trace.WriteLine( "Exiting CheckState for Person" );
#endif
}

使用#if 和#endif編譯選項(pragmas),你已經為你的發布版(release)編譯出了一個空方 法。這個CheckState()方法會在所有的版本(debug和release)中調用。而在 release中它什麼也不做,但它要被調用。因此你還是得為例行公事的調用它而 付出小部份代價。

不管怎樣,上面的實踐是可以正確工作的,但會導致 一個只會出現在release中的細小BUG。下面的就是一個常見的錯誤,它會告訴你 用條件編譯時會發生什麼:

public void Func( )
{
 string msg = null;
#if DEBUG
 msg = GetDiagnostics( );
#endif
 Console.WriteLine( msg );
}

這一切在 Debug模式下工作的很正常,但在release下卻輸出的為空行。release模式很樂 意給你輸出一個空行,然而這並不是你所期望的。傻眼了吧,但編譯器幫不了你 什麼。你的條件編譯塊裡的基礎代碼確實是這樣邏輯。一些零散的#if/#endif塊 使你的代碼在不同的編譯條件下很難得診斷(diagnose)。

C#有更好的選 擇:這就是條件屬性。用條件屬性,你可以在指定的編譯環境下廢棄一個類的部 份函數, 而這個環境可是某個變量是否被定義,或者是某個變量具有明確的值 。這一功能最常見的用法就是使你的代碼具有調試時可用的聲明。.Net框架庫已 經為你提供了了基本泛型功能。這個例子告訴你如何使用.net框架庫裡的兼容性 的調試功能,也告訴你條件屬性是如何工作的以及你在何時應該添加它:

當你建立了一個Person的對象時,你添加了一個方法來驗證對象的不變 數據(invariants):

private void CheckState( )
{
 // Grab the name of the calling routine:
 string methodName =
  new StackTrace( ).GetFrame( 1 ).GetMethod( ).Name;
  Trace.WriteLine( "Entering CheckState for Person:" );
 Trace.Write( "\tcalled by " );
 Trace.WriteLine( methodName );
 Debug.Assert( _lastName != null,
   methodName,
  "Last Name cannot be null" );
  Debug.Assert( _lastName.Length > 0,
  methodName,
   "Last Name cannot be blank" );
 Debug.Assert( _firstName != null,
  methodName,
  "First Name cannot be null" );
 Debug.Assert( _firstName.Length > 0,
  methodName,
  "First Name cannot be blank" );
 Trace.WriteLine( "Exiting CheckState for Person" );
}

這這個方法上,你可能不必用到太多的 庫函數,讓我簡化一下。這個StackTrace 類通過反射取得了調用方法的的名字 。這樣的代價是昂貴的,但它確實很好的簡化了工作,例如生成程序流程的信息 。這裡,斷定了CheckState所調用的方法的名字。被判定(determining)的方法 是System.Diagnostics.Debug類的一部份,或者是System.Diagnostics.Trace類 的一部份。Degbug.Assert方法用來測試條件是否滿足,並在條件為false時會終 止應用程序。剩下的參數定義了在斷言失敗後要打印的消息。Trace.WriteLine 輸出診斷消息到調試控制台。因此,這個方法會在Person對象不合法時輸出消息 到調試控制台,並終止應用程序。你可以把它做為一個先決條件或者後繼條件, 在所有的公共方法或者屬性上調用這個方法。

public string LastName
{
 get
 {
  CheckState( );
   return _lastName;
 }
 set
 {
  CheckState( );
  _lastName = value;
  CheckState( );
 }
}

在某人試圖給LastName賦空值或者null時,CheckState會在 第一時間引發一個斷言。然後你就可以修正你的屬性設置器,來為LastName的參 數做驗證。這就是你想要的。

但這樣的額外檢測存在於每次的例行任務 裡。你希望只在調試版中才做額外的驗證。這時候條件屬性就應運而生了:

[ Conditional( "DEBUG" ) ]
private void CheckState( )
{
 // same code as above
}

Conditional屬性會告訴C#編譯器,這個方法只在編譯環境變量 DEBUG有定義時才被調用。同時,Conditional屬性不會影響CheckState()函數生 成的代碼,只是修改對函數的調用。如果DEBGU標記被定義,你可以得到這:

public string LastName
{
 get
 {
   CheckState( );
  return _lastName;
 }
 set
 {
  CheckState( );
  _lastName = value;
   CheckState( );
 }
}

如果不是,你得到的就是這:

public string LastName
{
 get
 {
   return _lastName;
 }
 set
 {
  _lastName = value;
 }
}

不管環境變量的狀態如何, CheckState()的函數體是一樣的。這只是一個例子,它告訴你為什麼要弄明 白.Net裡編譯和JIT之間的區別。不管DEBUG環境變量是否被定義,CheckState() 方法總會被編譯且存在於程序集中。這或許看上去是低效的,但這只是占用一點 硬盤空間,CheckState()函數不會被載入到內存,更不會被JITed(譯注:這裡的 JITed是指真正的編譯為機器代碼),除非它被調用。它存在於程序集文件裡並不 是本質問題。這樣的策略是增強(程序的)可伸縮性的,並且這樣只是一點微不足 道的性能開銷。你可以通過查看.Net框架庫中Debug類而得到更深入的理解。在 任何一台安裝了.Net框架庫的機器上,System.dll程序集包含了Debug類的所有 方法的代碼。由環境變量在編譯時來決定是否讓由調用者來調用它們。

你同樣可以寫一個方法,讓它依懶於不只一個環境變量。當你應用多個環境變量 來控制條件屬性時,他們時以or的形式並列的。例如,下面這個版本的 CheckState會在DEBUG或者TRACE為真時被調用:

[ Conditional( "DEBUG" ),
 Conditional( "TRACE" ) ]
private void CheckState( )

如果要產生一個and的並列條件 屬性,你就要自己事先直接在代碼裡使用預處理命令定義一個標記:

#if ( VAR1 && VAR2 )
#define BOTH
#endif

是的,為了創建一個依懶於前面多個環境變量的條件例 程(conditional routine),你不得不退到開始時使用的#if實踐中了。#if為我 們產生一個新的標記,但避免在編譯選項內添加任何可運行的代碼。

Conditional屬性只能用在方法的實體上,另外,必須是一個返回類型為 void的方法。你不能在方法內的某個代碼塊上使用Conditional,也不能在一個 有返回值的方法上使用Conditional屬性。取而代之的是,你要細心構建一個條 件方法,並在那些方法上廢棄條件屬性行為。你仍然要回顧一下那些具有條件屬 性的方法,看它是否對對象的狀態具有副作用。但Conditional屬性在安置這些 問題上比#if/#endif要好得多。在使用#if/#endif塊時,你很可能錯誤的移除了 一個重要的方法調用或者一些配置。

前面的例子合用預先定義的DEBUG或 者TRACE標記,但你可以用這個技巧,擴展到任何你想要的符號上。Conditional 屬性可以由定義標記來靈活的控制。你可以在編譯命令行上定義,也可以在系統 環境變量裡定義,或者從源代碼的編譯選擇裡定義。

使用Conditional屬 性可以比使用#if/#endif生成更高效的IL代碼。在專門針對函數時,它更有優勢 ,它會強制你在條件代碼上使用更好的結構。編譯器使用Conditional屬性來幫 助你避免因使用#if/#endif而產生的常見的錯誤。條件屬性比起預處理,它為你 區分條件代碼提供了更好的支持。

小結:翻譯了幾篇了 ,感覺書寫的有點冗余,有些問題可以很簡單的說明的。可能是我的理解不到位 ,總之,感覺就是一個問題說來說去。另外,這裡例舉的幾個例子感覺也不是很 好,特別是前一個Item裡的強制轉化,感覺很牽強。不管怎樣,還是認真的把書 讀好,譯好吧。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved