Effective C# 原則4:用條件屬性而不是#if先行編譯塊(譯)

來源:互聯網
上載者:User

Item 4: Use Conditional Attributes Instead of #if
原則4:用條件屬性而不是#if

使用#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裡的強制轉化,感覺很牽強。不管怎樣,還是認真的把書讀好,譯好吧。
還是那句話,或者我翻譯的不好,或者網上已經有更好的翻譯了,或者中文版也出來了,但我還是會堅持翻譯下去。但以後的翻譯不會再放在部落格園的首頁了。擔心自己的翻譯不好,以後的翻譯都帶上原文。總之,希望對讀者有協助!

分開了一個子類,專門POST Effective C#的翻譯。

http://www.cnblogs.com/WuCountry/category/85054.html

===============================

Item 4: Use Conditional Attributes Instead of #if
#if/#endif blocks have been used to produce different builds from the same source, most often debug and release variants. But these have never been a tool we were happy to use. #if/#endif blocks are too easily abused, creating code that is hard to understand and harder to debug. Language designers have responded by creating better tools to produce different machine code for different environments. C# has added the Conditional attribute to indicate whether a method should be called based on an environment setting. It's a cleaner way to describe conditional compilation than #if/#endif. The compiler understands the Conditional attribute, so it can do a better job of verifying code when conditional attributes are applied. The conditional attribute is applied at the method level, so it forces you to separate conditional code into distinct methods. Use the Conditional attribute instead of #if/#endif blocks when you create conditional code blocks.

Most veteran programmers have used conditional compilation to check pre- and post-conditions in an object. You would write a private method to check all the class and object invariants. That method would be conditionally compiled so that it appeared only in your debug builds.

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
}

 

Using the #if and #endif pragmas, you've created an empty method in your release builds. The CheckState() method gets called in all builds, release and debug. It doesn't do anything in the release builds, but you pay for the method call. You also pay a small cost to load and JIT the empty routine.

This practice works fine, but can lead to subtle bugs that appear only in release builds. The following common mistake shows what can happen when you use pragmas for conditional compilation:

public void Func( )
{
  string msg = null;

#if DEBUG
  msg = GetDiagnostics( );
#endif
  Console.WriteLine( msg );
}

 

Everything works fine in your debug build, but the release builds print a blank line. Your release builds happily print a blank message. That's not your intent. You goofed, but the compiler couldn't help you. You have code that is fundamental to your logic inside a conditional block. Sprinkling your source code with #if/#endif blocks makes it hard to diagnose the differences in behavior with the different builds.

C# has a better alternative: the Conditional attribute. Using the Conditional attribute, you can isolate functions that should be part of your classes only when a particular environment variable is defined or set to a certain value. The most common use of this feature is to instrument your code with debugging statements. The .NET Framework library already has the basic functionality you need for this use. This example shows how to use the debugging capabilities in the .NET Framework Library, to show you how Conditional attributes work and when to add them to your code.

When you build the Person object, you add a method to verify the object 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" );
}

 

You might not have encountered many library functions in this method, so let's go over them briefly. The StackTrace class gets the name of the calling method using Reflection (see Item 43). It's rather expensive, but it greatly simplifies tasks, such as generating information about program flow. Here, it determines the name of the method that called CheckState. The remaining methods are part of the System.Diagnostics.Debug class or the System.Diagnostics.Trace class. The Debug.Assert method tests a condition and stops the program if that condition is false. The remaining parameters define messages that will be printed if the condition is false. trace.WriteLine writes diagnostic messages to the debug console. So, this method writes messages and stops the program if a person object is invalid. You would call this method in all your public methods and properties as a precondition and a post-condition:

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

 

CheckState fires an assert the first time someone tries to set the last name to the empty string, or null. Then you fix your set accessor to check the parameter used for LastName. It's doing just what you want.

But this extra checking in each public routine takes time. You'll want to include this extra checking only when creating debug builds. That's where the Conditional attribute comes in:

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

 

The Conditional attribute tells the C# compiler that this method should be called only when the compiler detects the DEBUG environment variable. The Conditional attribute does not affect the code generated for the CheckState() function; it modifies the calls to the function. If the DEBUG symbol is defined, you get this:

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

 

If not, you get this:

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

 

The body of the CheckState() function is the same, regardless of the state of the environment variable. This is one example of why you need to understand the distinction made between the compilation and JIT steps in .NET. Whether the DEBUG environment variable is defined or not, the CheckState() method is compiled and delivered with the assembly. That might seem inefficient, but the only cost is disk space. The CheckState() function does not get loaded into memory and JITed unless it is called. Its presence in the assembly file is immaterial. This strategy increases flexibility and does so with minimal performance costs. You can get a deeper understanding by looking at the Debug class in the .NET Framework. On any machine with the .NET Framework installed, the System.dll assembly does have all the code for all the methods in the Debug class. Environment variables control whether they get called when callers are compiled.

You can also create methods that depend on more than one environment variable. When you apply multiple conditional attributes, they are combined with OR. For example, this version of CheckState would be called when either DEBUG or trACE is TRue:

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

 

To create a construct using AND, you need to define the preprocessor symbol yourself using preprocessor directives in your source code:

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

 

Yes, to create a conditional routine that relies on the presence of more than one environment variable, you must fall back on your old practice of #if. All #if does is create a new symbol for you. But avoid putting any executable code inside that pragma.

The Conditional attribute can be applied only to entire methods. In addition, any method with a Conditional attribute must have a return type of void. You cannot use the Conditional attribute for blocks of code inside methods or with methods that return values. Instead, create carefully constructed conditional methods and isolate the conditional behavior to those functions. You still need to review those conditional methods for side effects to the object state, but the Conditional attribute localizes those points much better than #if/#endif. With #if and #endif blocks, you can mistakenly remove important method calls or assignments.

The previous examples use the predefined DEBUG or trACE symbols. But you can extend this technique for any symbols you define. The Conditional attribute can be controlled by symbols defined in a variety of ways. You can define symbols from the compiler command line, from environment variables in the operating system shell, or from pragmas in the source code.

The Conditional attribute generates more efficient IL than #if/#endif does. It also has the advantage of being applicable only at the function level, which forces you to better structure your conditional code. The compiler uses the Conditional attribute to help you avoid the common errors we've all made by placing the #if or #endif in the wrong spot. The Conditional attribute provides better support for you to cleanly separate conditional code than the preprocessor did.
 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.