# 原則4:使用條件特性(conditional attribute)代替 #if
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明 [出處](/blog/1979093) :[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
\#if/\#endif 塊在相同的代碼會編譯成不同的版本,大多數會有調試(debug)和發布(release)兩個版本。但我們會很不樂意去使用這個工具。 \#if/\#endif 塊很容易被濫用,寫的代碼很難理解而且不容易調試。語言設計者應該負責設計能在不同環境產生不同的機器碼的工具。 C# 提供了條件特性(conditional attribute),可以根據設置的環境決定函數的調用。使用條件特性會使用 #if/endif 更加清晰。編譯器能解析條件特性,所以能很好的確定那段代碼被調用。條件特性是應用在函數級別的,所以你應該分開不同的條件代碼封裝成不同的方法。當你要使用條件代碼塊的時候,使用條件特性代替 #if/endif 。
很多資深的程序員喜歡使用條件編譯來檢查對象的先決和后續條件。你會寫一個 private 方法去檢查所有類型和對象變量。這個方法使用了條件編譯以至于只能在調試版本出現。
```
private void CheckStateBad()
{
// 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 編譯選項,你會發現在發布版本你實際創建了一個空的方法。 CheckState() 會被所有版本中調用,比如發布版本和調試版本。在發布版本沒有做任何事情,但你為此付出了函數調用的代價。同時還要有很小花費在加載和 JIT 空程序。
這個實踐是正確的,但在發行版本會導致一個微妙的錯誤。下面這個就是使用條件編譯選項的常見錯誤:
```
public void Func()
{
string msg = null;
#if DEBUG
msg = GetDiagnostics();
#endif
Console.WriteLine(msg);
}
```
在調試版本會正確工作,但是你的發行版本只會讓你哭笑不得地輸出空字符串。當然這不是你想要的。你出了錯,編譯器就幫不了你了。在條件編譯器塊中的代碼就是你的邏輯。在 #if/endif 塊中的代碼很難讓你診斷出不同版本的不同行為。
C# 有一個更好的選擇:條件特性。使用條件特性,能分離出不同函數,只有在特定的環境變量的定義或某些值的設置才會屬于你的類。這個功能最常見的好處就是在調試的時候能有可用的聲明。 .NET 框架已經提供了基本的通用功能。這個例子告訴我們 .NET 框架的調試功能,已經條件特性是怎么工作的和什么時候添加到你的代碼中。
當你創建了 Person 對象,你要寫一個方法去檢查對象的變量:
```
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 。如果被 inline 調用會有一個小的風險,另一種方法就是在調用 CheckState 函數的方法使用 MethodBase.GetCurrentMethod() 傳入方法名。你很快會明白為什么不使用這個策略。
后面的方法是 System.Diagnositics.Debug 類或 System.Diagnostics.Trace 類的函數。 Debug.Assert 測試條件,且當條件為 false 是程序停止。后面的是條件為 false 是打印出來的信息。 Trace.WriteLine 將診斷信息輸出到調試控制臺上。所以這個方法實際當 person 對象不正確是輸出信息并終止程序。你可以在所有 public 方法或屬性中調用這個方法作為先決條件和后續條件:
```
public string LastName
{
get
{
CheckState();
return lastName;
}
set
{
CheckState();
lastName = value;
CheckState();
}
}
```
如果將一個空字符串或 null 賦給 lastName , CheckState 觸發一個斷言(assert)。然后檢驗 lastName 的值。這就是你想要做的。
但這額外的檢查會在每個例行任務中花費時間。你只是在調試版本中需要額外的檢查。那就是為什么會有條件特性:
```
[Conditional("DEBUG")]
private void CheckState()
{
// same code as above
}
```
條件特性告訴 C# 編譯器這個方法只能在有 DEBUG 變量的環境中被調用。
條件特性不影響 CheckState 函數代碼的產生,修改的是調用者的代碼。如果 DEBUG 變量被定義,你的代碼是這樣的:
```
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 ,除非被調用。它存在在程序集的二進制文件中是無關緊要的。這個策略增加靈活性而且不消耗性能。閱讀 .NET 框架的 Debug 類可以有更加深入的理解。安裝了 .NET 框架的機器, System.dll 程序集會有 Debug 類的所有方法代碼。當調用者函數被編譯,環境變量控制方法是否被調用。運用條件指令可以讓你創建的類庫嵌入調試特性。這些特性可以運行時打開或關閉。
你可以創建一個方法依賴多個環境變量。當你運用多個條件特性,它們是通過 OR 組合起來的。例如,下面版本的 CheckState 當 DEBUG 或 TRACE 為真時,會調用。
```
[Conditional("DEBUG"),Conditional("TRACE")]
private void CheckState()
```
如果想要使用 AND 構建,你需要使用預處理指令定義預處理符:
```
#if ( VAR1 && VAR2 )
#define BOTH
#endif
```
的確,當你要創建一個依賴多于一個環境變量的條件行為,你不得不退回到之前 #if 的做法。所有 #if 創建一個符號。但要避免在編譯選項中加入任何可運行代碼。
所以,你可以按下面方式重寫 CheckState 方法:
```
private void CheckStateBad()
{
// The Old way:
#if BOTH
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
}
```
條件特性只能運用于整個的方法。除此之外,任何條件特性方法必須是 void 返回值。你在代碼塊中使用條件特性,也不能創建有返回值的條件特性方法。而是,要分離出具體條件行為的代碼單獨寫成條件特性方法。你應該回顧下那些條件方法對對象狀態的副作用,條件特性會比 #if/endif 好很多。使用 #if/endif 塊,你會錯誤的移除了很多重要的方法調用或賦值語句。
前面的例子使用預定義的 DEBUG 或 TRACE 符號。你也可以擴展任何你定義的符號。條件特性可以被多種方式定義的符號控制。你可以定義符號從編譯命令行,或從操作系統 shell 的環境變量,或從代碼的編譯選項中定義。
你應該注意到前面的每個條件特性的方法都是 void 返回值而且沒有參數。實際實踐應該遵從這個原則。編譯器是前置條件特性方法必須是 void 返回值。然后,你可以創建一個方法含有引用類型參數。這種做法會導致副作用,應該盡量避免。考慮下面一段代碼:
```
Queue<string> names = new Queue<string>();
names.Enqueue("one");
names.Enqueue("two");
names.Enqueue("three");
string item = string.Empty;
SomeMethod(item = names.Dequeue());
Console.WriteLine(item);
```
SomeMethod 添加了條件特性:
```
[Conditional("DEBUG")]
private static void SomeMethod(string param)
{
}
```
這里會有一個很微妙的錯誤。 SomeMethod() 只有在 DEBUG 符號被定義了才會被調用。而且 names.Dequeue() 也是一樣的。因為結果不是必須的,所以方法沒有調用。任何條件特性的方法不應該有任何參數。使用調用方法來產生參數會有副作用。如果條件不為 ture 這些方法不會被調用。
條件特性比 \#if/\#endif 產生了更高效的 IL 代碼。還有一個好處就是只能使用在函數級別上,這迫使你要更好的組織你的條件代碼。編譯器使用條件特性幫助我們避免了使用 \#if/\#endif 的常見錯誤。條件特性比預處理更能讓你你的條件代碼分離的更清晰。
小結:
更新晚了,昨天晚上寫了一半,現在弄完。昨天家里發生了點事情,心里一直不安,感覺挺無奈的。只有對自己說,我要努力,我要頂住。原則4,相對于前面3個原則有點偏門,而且兩點少了點,說服力不夠。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1979093](/blog/1979093)
更多精彩請關注D.S.Qiu的 博客 和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數