多用類型常量,少用#define預處理指令

注:本文整理自《Effective Objective-C 2.0編寫高質量iOS 與 OS X代碼的52個有效方法》 

編寫代碼時經常要定義常量。例如,要寫一個UI視圖類,此視圖顯示出來之後就播放動畫,然後消失。你可能想把播放動畫的時間提取爲常量。掌握了Objective-C與其C語言基礎的人,也許會用這種方法來做:
 

  1. #define ANIMATION_DURATION 0.3 

上述預處理指令會把源代碼中的ANIMATION_DURATION字符串替換爲0.3。這可能就是你想要的效果,不過這樣定義出來的常量沒有類型信息。“持續”(duration)這個詞看上去應該與時間有關,但是代碼中又未明確指出。此外,預處理過程會把碰到的所有ANIMATION_DURATION一律替換成0.3,這樣的話,假設此指令聲明在某個頭文件中,那麼所有引入了這個頭文件的代碼,其ANIMATION_DURATION都會被替換。

要想解決此問題,應該設法利用編譯器的某些特性纔對。有個辦法比用預處理指令來定義常量更好。比方說,下面這行代碼就定義了一個類型爲NSTimeInterval的常量:
 

  1. static const NSTimeInterval kAnimationDuration = 0.3; 

請注意,用此方式定義的常量包含類型信息,其好處是清楚地描述了常量的含義。由此可知該常量類型爲NSTimeInterval,這有助於爲其編寫開發文檔。如果要定義許多常量,那麼這種方式能令稍後閱讀代碼的人更易理解其意圖。

還要注意常量名稱。常用的命名法是:若常量侷限於某“編譯單元”(translation unit,也就是“實現文件”,implementation file)之內,則在前面加字母k;若常量在類之外可見,則通常以類名爲前綴。第19條詳解了命名習慣(naming convention)。

定義常量的位置很重要。我們總喜歡在頭文件裏聲明預處理指令,這樣做真的很糟糕,當常量名稱有可能互相沖突時更是如此。例如,ANIMATION_DURATION這個常量名就不該用在頭文件中,因爲所有引入了這份頭文件的其他文件中都會出現這個名字。其實就連用static const定義的那個常量也不應出現在頭文件裏。因爲Objective-C沒有“名稱空間”(namespace)這一概念,所以那樣做等於聲明瞭一個名叫kAnimationDuration的全局變量。此名稱應該加上前綴,以表明其所屬的類,例如可改爲EOCViewClassAnimationDuration。本書第19條中深入講解了一套清晰的命名方案。

若不打算公開某個常量,則應將其定義在使用該常量的實現文件裏。比方說,要開發一個使用UIKit框架的iOS應用程序,其UIView子類中含有表示動畫播放時間的常量,那麼可以這樣寫:
 

  1. // EOCAnimatedView.h  
  2. #import <UIKit/UIKit.h> 
  3.  
  4. @interface EOCAnimatedView : UIView  
  5. - (void)animate;  
  6. @end  
  7.  
  8. // EOCAnimatedView.m  
  9. #import "EOCAnimatedView.h"  
  10.  
  11. static const NSTimeInterval kAnimationDuration = 0.3;  
  12.  
  13. @implementation EOCAnimatedView  
  14. - (void)animate {  
  15.     [UIViewanimateWithDuration:kAnimationDuration  
  16.                     animations:^(){  
  17.                          // Perform animations  
  18.                     }];  
  19. }  
  20. @end 

變量一定要同時用static與const來聲明。如果試圖修改由const修飾符所聲明的變量,那麼編譯器就會報錯。在本例中,我們正是希望這樣:因爲動畫播放時長爲定值,所以不應修改。而static修飾符則意味着該變量僅在定義此變量的編譯單元中可見。編譯器每收到一個編譯單元,就會輸出一份“目標文件”(object file)。在Objective-C的語境下,“編譯單元”一詞通常指每個類的實現文件(以.m爲後綴名)。因此,在上述範例代碼中聲明的kAnimationDuration變量,其作用域僅限於由EOCAnimatedView.m所生成的目標文件中。假如聲明此變量時不加static,則編譯器會爲它創建一個“外部符號”(external symbol)。此時若是另一個編譯單元中也聲明瞭同名變量,那麼編譯器就拋出一條錯誤消息:
 

  1. duplicate symbol _kAnimationDuration in:  
  2.     EOCAnimatedView.o  
  3.     EOCOtherView.o 

實際上,如果一個變量既聲明爲static,又聲明爲const,那麼編譯器根本不會創建符號,而是會像#define預處理指令一樣,把所有遇到的變量都替換爲常值。不過還是要記住:用這種方式定義的常量帶有類型信息。

有時候需要對外公開某個常量。比方說,你可能要在類代碼中調用NSNotificationCenter以通知他人。用一個對象來派發通知,令其他欲接收通知的對象向該對象註冊,這樣就能實現此功能了。派發通知時,需要使用字符串來表示此項通知的名稱,而這個名字就可以聲明爲一個外界可見的常值變量(constant variable)。這樣的話,註冊者無須知道實際字符串值,只需以常值變量來註冊自己想要接收的通知即可。

此類常量需放在“全局符號表”(global symbol table)中,以便可以在定義該常量的編譯單元之外使用。因此,其定義方式與上例演示的static const有所不同。應該這樣來定義:
 

  1. // In the header file  
  2. extern NSString *const EOCStringConstant;  
  3.  
  4. // In the implementation file  
  5. NSString *const EOCStringConstant = @"VALUE"; 

這個常量在頭文件中“聲明”,且在實現文件中“定義”。注意const修飾符在常量類型中的位置。常量定義應從右至左解讀,所以在本例中,EOCStringConstant就是“一個常量,而這個常量是指針,指向NSString對象”。這與需求相符:我們不希望有人改變此指針常量,使其指向另一個NSString對象。

編譯器看到頭文件中的extern關鍵字,就能明白如何在引入此頭文件的代碼中處理該常量了。這個關鍵字是要告訴編譯器,在全局符號表中將會有一個名叫EOCStringConstant的符號。也就是說,編譯器無須查看其定義,即允許代碼使用此常量。因爲它知道,當鏈接成二進制文件之後,肯定能找到這個常量。

此類常量必須要定義,而且只能定義一次。通常將其定義在與聲明該常量的頭文件相關的實現文件裏。由實現文件生成目標文件時,編譯器會在“數據段”(data section)爲字符串分配存儲空間。鏈接器會把此目標文件與其他目標文件相鏈接,以生成最終的二進制文件。凡是用到EOCStringConstant這個全局符號的地方,鏈接器都能將其解析。

因爲符號要放在全局符號表裏,所以命名常量時需謹慎。例如,某應用程序中有個處理登錄操作的類,在登錄完成後會發出通知。派發通知所用的代碼如下:
 

  1. // EOCLoginManager.h  
  2. #import <Foundation/Foundation.h> 
  3.  
  4. extern NSString *const EOCLoginManagerDidLoginNotification;  
  5.  
  6. @interface EOCLoginManager : NSObject  
  7. - (void)login;  
  8. @end  
  9.  
  10. // EOCLoginManager.m  
  11. #import "EOCLoginManager.h"  
  12.  
  13. NSString *const EOCLoginManagerDidLoginNotification =  
  14.     @"EOCLoginManagerDidLoginNotification";  
  15.  
  16. @implementation EOCLoginManager  
  17.  
  18. - (void)login {  
  19.     // Perform login asynchronously, then call 'p_didLogin'.  
  20. }  
  21.  
  22. - (void)p_didLogin {  
  23.     [[NSNotificationCenter defaultCenter]  
  24.       postNotificationName:EOCLoginManagerDidLoginNotification  
  25.                    object:nil];  
  26. }  
  27. @end 

注意常量的名字。爲避免名稱衝突,最好是用與之相關的類名做前綴。系統框架中一般都這樣做。例如UIKit就按照這種方式來聲明用作通知名稱的全局常量。其中有類似UIApplicationDidEnterBackgroundNotification與UIApplicationWillEnterForegroundNotification這樣的常量名。

其他類型的常量也是如此。假如要把前例中EOCAnimatedView類裏的動畫播放時長對外公佈,那麼可以這樣聲明:
 

  1. // EOCAnimatedView.h  
  2. extern const NSTimeInterval EOCAnimatedViewAnimationDuration;  
  3.  
  4. // EOCAnimatedView.m  
  5. const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3; 

這樣定義常量要優於使用#define預處理指令,因爲編譯器會確保常量值不變。一旦在EOCAnimatedView.m中定義好,即可隨處使用。而採用預處理指令所定義的常量可能會無意中遭人修改,從而導致應用程序各個部分所使用的值互不相同。

總之,勿使用預處理指令定義常量,而應該藉助編譯器來確保常量正確,比方說可以在實現文件中用static const來聲明常量,也可以聲明一些全局常量。

要點

不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量值不一致。

在實現文件中使用static const來定義“只在編譯單元內可見的常量”(translation-unit-specific constant)。由於此類常量不在全局符號表中,所以無須爲其名稱加前綴。

在頭文件中使用extern來聲明全局常量,並在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴。

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