第一章 熟悉Objective-C—第4條:多用型別常量,少用#define預處理指令

weixin_34292287發表於2017-03-06

編寫程式碼時經常要定義變數。例如:

#define ANIMATION_DURATION 0.3

上述預處理指令會把原始碼中的ANIMATION_DURATION字串替換為0.3。這可能就是你想要的效果,不過這樣定義出來的常量沒有型別資訊。"持續"(duration)這個詞看上去應該與時間有關,但是程式碼中又未明確指出。此外,預處理過程會把碰到的所有ANIMATION_DURATION一律替換成0.3,這樣的話,假設此指令宣告在某個標頭檔案中,那麼所有引入了這個標頭檔案的程式碼,其ANIMATION_DRRATION都會被替換。
要想解決此問題,應該設法利用編譯器的某些特性才對。有個辦法比用預處理指令來定義常量更好。比方說,下面這行程式碼就定義了一個型別為NSTimeInterval的常量:

static const NSTimeInterval kAnimationDuration = 0.3;

請注意,用此方式定義的常量包含型別資訊,其好處是清楚地描述了常量的含義。由此可知該常量型別為NSTimeInterval,這有助於為其編寫開發文件。如果要定義許多常量,那麼這種方式能令稍後閱讀程式碼的人更易理解其意圖。
還要注意常量名稱。常用的命名法是:若常量侷限於某"編譯單元"(translation unit,也就是"實現檔案",implementation file)之內,則在前面加字母k;若常量在類之外可見,則通常以類名為字首。
定義常量的位置很重要。我們總喜歡在標頭檔案裡宣告預處理命令,這樣做真的很糟糕,當常量名稱有可能互相沖突時更是如此。例如,ANIMATION_DURATION這個常量名就不該用在標頭檔案中,因為所有引入了這份標頭檔案的其他檔案中都會出現這個名字。其實就連用static const定義的那個常量也不該出現在標頭檔案裡。因為Objective-C沒有"名稱空間"(namespace)這一概念,所以那樣做等於宣告瞭一個名叫kAnimationDuration的全域性變數。此名稱應該加上字首,以表明其所屬的類,例如可改為EOCViewClassAnimationDuration。
若不打算公開某個常量,則應將其定義在使用該常量的實現檔案裡。比方說,要開發一個使用UIKit框架的iOS應用程式,其UIView子類中含有表示動畫播放時間的常量,那麼可以這樣寫:

//EOCAnimatedView.h
#import <UIKit/UIKit.h>

@interface EOCAnimatedView : UIView
- (void)animate;
@end

//EOCAnimatedView.m
#import "EOCAnimatedView.h"

static const NSTimeInterval kAnimationDuration = 0.3;

@implementation EOCAnimatedView
- (void)animate {
  [UIView animateWithDuration:kAnimationDuration
                        animations:^(){
                          //Perform animations
      }];
}
@end

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

duplicate symbol _kAnimationDuration in:
EOCAnimatedView.o
EOCOtherView.o

實際上,如果一個變數既宣告為static,又宣告為const,那麼編譯器根本不會建立符號,而是會像#define預處理指令一樣,把所有遇到的變數都替換為常值。不過還是要記住:用這種方式定義的常量帶有型別資訊。
有時候需要對外公開某個常量。比方說,你可能要在類程式碼中呼叫NSNotificationCenter以通知他人。用一個物件來派發通知,令其他欲接收通知的物件向該物件註冊,這樣就能實現此功能了。派發通知時,需要使用字串來表示此項通知的名稱,而這個名字就可以宣告為一個外界可見的常值變數(constant variable)。這樣的話,註冊者無須知道實際字串值,只需以常值變數來註冊自己想要接收的通知即可。
此類常量需放在"全域性符號表"(global symbol table)中,以便可以在定義該常量的編譯單元之外使用。因此,其定義方式與上例演示的static const有所不同。應該這樣來定義:

//In the header file
extern NSString *const EOCStringConstant;

//In the implementation file
NSString *const EOCStringConstant = @"VALUE";

這個常量在標頭檔案中"宣告",且實現檔案中"定義"。注意const修飾符在常量型別中的位置。常量定義應從右至左解讀,所以在本例中,EOCStringConstant就是"一個常量,而這個常量是指標,指向NSString物件"。這與需求相符:我們不希望有人改變此指標常量,使其指向另一個NSString物件。
編譯器看到標頭檔案中的extern關鍵字,就能明白如何在引入此標頭檔案的程式碼中處理該常量了。這個關鍵字是要告訴編譯器,在全域性符號表中將會有一個名叫EOCStringConstant的符號。也就是說,編譯器無須檢視其定義,即允許程式碼使用此常量。因為它知道,當連結成二進位制檔案之後,肯定能找到這個常量。
此類常量必須要定義,而且只能定義一次。通常將其定義在與宣告該常量的標頭檔案相關的實現檔案裡。由實現檔案生成目標檔案時,編譯器會在"資料段"(data section)為字串分配儲存空間。連結器會把此目標檔案與其他目標檔案相連結,以生成最終的二進位制檔案。凡是用到EOCStringConstant這個全域性符號的地方,連結器都能將其解析。
因為符號要放在全域性符號表裡,所以命名常量時需謹慎。

總之,勿使用預處理指令定義常量,而應該藉助編譯器來確保常量正確,比方說可以在實現檔案中用static const來宣告常量,也可以宣告一些全域性常量。

相關文章