如何寫優雅的代碼(2)——#define?const?還是enum?

    //========================================================================
    //TITLE:
    //    如何寫優雅的代碼(2)——#define?const?還是enum?
    //AUTHOR:
    //    norains
    //DATE:
    //    Tuesday  21-July-2009
    //Environment:
    //    WINCE5.0 + VS2005
    //========================================================================

    #define,const,enum:這三者有何關聯?一個是宏定義,一個是靜態修飾符,最後一個還是枚舉類型。是不是有點像養麥皮打漿糊——粘不到一起?如果我們將範圍縮小再縮小,讓三者都只侷限於“固定值”,那麼千絲萬縷的關係就瞭然於紙上——至少,有共同點了。
   
    在解釋什麼是“固定值”之前,我們先來了解何爲“奇數”。太多的原則都有告誡,少用“奇數”,因爲這將導致代碼不可維護。聽起來似乎如算命的釋語般玄之又玄,不可捉摸,但其間的語義卻是如此簡單。下面這兩個代碼段,正好說明“奇數”之糟糕:
   

 

    顯而易見,代碼段2的可讀性比代碼段1要高多了。在這兩個實例裏,像“1”,“2”,“3”這種就叫奇數,而“SLEEP”,“POWER_OFF”,“POWER_ON”就是固定值。固定值的定義在C++中有三種方式,分別就是本文要討論的#define,const和enum。
   
    大名鼎鼎的《Effect C++》的作者Scott Meyers就曾建議過,凡是用const能代替#define的地方,都應該用const。這句話不無道理,也從另一方面來說,#define和const事實上很多地方都能互用。
   
    比如:

 

    無論你是用const還是#define來定義DEFAULT_VOLUME,對於m_dwVolume = DEFAULT_VOLUME這語句而言都沒有本質性的變化。那麼,是不是意味着,是用#define還是用const,完全取決於當時的心情了?答案自然是否定的,否則本文就成了抒情散文了。
   
    #define有個致命的缺陷,不受作用域限制。凡是在#define之後的代碼,都可以直接使用#define定義的數值。
   
    我們經常會寫這麼一個函數,用以獲取某個設備的DWORD值。但這個函數不是返回BOOL類型來表示成敗,而是採用另外一種方式:當讀取成功時,返回的是具體和設備有關的數值;當失敗時,返回的是默認數值。聽起來這函數功能有點奇怪,也懷疑在什麼情況下才會採用如此設計,但可惜本文主題不是討論該函數能幹什麼,或應該出現於什麼地點,我們只要知道有這麼一種函數即可。
   
    我們姑且假設這函數原型如下:

 

    調用也很簡單:

 


    在這個例子中,如果dwVal的數值等於ERROR_VALUE,那麼意味着調用GetDevDW失敗;不等於ERROR_VALUE才意味着調用成功。
   
    現在我們有兩個函數,分別用來獲取兩個設備的信息。在接下來的例子中,我們採用#define來定義固定值:

 

    看起來一切似乎都挺好,難道不是嘛?只可惜,編譯會有警告出現:'ERROR_VALUE' : macro redefinition。
   
    問題的根源只在於#define的數值沒有作用域的概念。更爲糟糕的是,在GetDev2Info函數中使用的ERROR_VALUE並不是我們所期望的2,而是在GetDev1Info中定義的0。噢,我的天,再也沒有比這更糟糕的事了。
   
    爲了徹底解決這個警告,我們可以在GetDev2Info函數做一些額外的工作:

 

    問題解決了,警告沒有了,但代碼卻醜陋了。
   
    還有另一種方式,更改固定值的名稱:

 

    同樣,問題解決了,警告沒有了,並且,代碼也不算醜陋。遺留的唯一問題是,如果類似函數很多的話,我們需要絞盡腦汁去給每個錯誤固定值選擇一個唯一的名字。呃,這對於我們這些懶人而言,這並不算一個好差事。既然如此,爲什麼不用const呢?

 

    沒錯,僅此而已。因爲const DWORD聲明的是一個局部變量,受限於作用域的侷限,所以我們在GetDev1Info和GetDev2Info都能使用相同的固定值名稱。
   
    這個例子也許還不足以說服你用const替代#define,那麼接下來的例子你應該會扭轉這一觀念——或許這例子你已經碰到過。
   
    我們有兩個class,分別用來控制汽車的重音和功放。這兩個類都需要在頭文件中定義MAX_VOLUME以供使用者調用,但很不幸的是,重音和功放的MAX_VOLUME值是不同的。
   
    如果用#define,在頭文件中我們可能這麼寫:

 

    當兩個頭文件沒有同時使用時,一切都很順利,不是嘛?
   
    但如果我需要同時控制着兩個音量,那麼我們就必須要同時include這兩個文件,像這種調用大家應該不陌生吧:

 

    那麼問題就很顯然:嚴重的警告或是無法通過編譯。
   
    爲了解決這個問題,我們還是隻能請出const。只不過,如果還是簡單地聲明如下:

 

    那麼該出現的問題還是和用#define一樣,沒有任何本質上的改變。這時候,我們只能請出namespace了。

 

    在沒有使用using來省略命名空間的情況下,我們可以這麼折騰代碼:

 

    在這個例子中,命名空間起到標誌作用,標明當前的MAX_VOLUME屬於哪種範疇,也算意外的收穫。
   
    看到這裏,也許有人會問,如果是namespace + #define方式可以麼?很遺憾,答案是不行。正如前面所說,#define不受限於作用域,所以簡簡單單的namespace無法套住#define這隻猛獸。
   
    至此,我們可以這麼下定論,在不涉及到條件編譯,並且只是使用固定值的前提下,我們都應該用const來替代#define。
   
    基於這個原則,以下的討論我們就拋開#define,只用const。
   
    我們再回過頭來看看文章最初的例子,將其封裝爲一個函數:

 

    在代碼的他處定義瞭如下固定值:

 

    調用的時候:

 

    很好,很漂亮,難道不是麼?
   
    但這樣子無法保證使用者不是如此調用代碼:

 

    0x100不是我們想要的數值,在SwitchMode函數也不會對該數值有相應的處理,但偏偏這符合編譯器的規範,它會讓這代碼沒有任何警告沒有任何錯誤順利編譯通過。
   
    也許還有人說,誰會那麼傻,直接用0x100來賦值啊?這話確實沒錯,直接用0x100的概率確實太少了。
   
    但我們無法否認,會有這麼一種可能:有另外一個函數,其中一個固定值爲如下定義:

 

    而我們一時衝昏了頭,又或許喝醉了酒,將該參數誤用了:

 

    對於編譯器來說,無論是0x100還是FILE_MODE,都沒有太多意義,所以這病態代碼很容易通過編譯器檢測;而對於人而言,因爲已經使用了固定值,也下意識以爲這參數是符合的。兩者,無論是編譯器,還是我們,都被合理地矇騙了。
       
    那麼,我們有辦法在編譯的時候,如果該數值不是我們所想要的,編譯器能給使用者提示警告甚至錯誤麼?
   
    一切皆有可能!不過,這時候我們不能使用const,而必須換用enum。
   
    首先用enum定義固定值:

 

    函數的聲明如此更換:

 

    調用也是和之前無異:

 

    唯一的不同就是,如果你這樣調用:

 

    那麼編譯器就會毫不猶豫地發出抱怨:cannot convert parameter 1 from 'int' to 'Mode'。
   
    很好,編譯器已經作爲我們的第一道防火牆,將我們所不需要的毫無關聯的數值通通排除在外。難道不是很美好嗎?
   
    當然,如果你想強制讓編譯器通過異樣的數值也不是不可能:

 

    雖然0x100不處於Mode的範圍之內,但依然還是通過了編譯器的檢測。對此,我們毫無辦法。只是,像這種極端的異教徒的做法,有多少情況下會碰到呢?
   
   
    最後的最後,我們略微總結一下:
   
    1.只是聲明單一固定值,儘可能採用const。
   
    2.如果是一組固定值,並且互相有關聯,則採用enum。
   
    3.不涉及條件編譯,只是定義固定值的情形下,儘可能不使用#define。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章