標籤:

書單1:第4條:多用類型常量,少用#define預處理指令

書單1:第4條:多用類型常量,少用#define預處理指令

1.編寫代碼時經常要定義常量。例如,要寫一個UI視圖類,此視圖顯示出來之後就播放動畫,然後消失。你可能想把播放動畫的時間提取為常量。如:

#define ANIMATION_DURATION 0.3

上述預處理指令會把源代碼中的ANIMATION_DURATION字元串替換為0.3,不過這樣定義出來的常量沒有類型信息。「持續duration」這個詞看上去應該與時間有關,但是代碼中又未明確指出。此外,預處理過程會把碰到的所有ANIMATION_DURATION一律替換成0.3,這樣的話,假設此指令聲明在某個頭文件中,那麼所有引入了這個頭文件的代碼,其ANIMATION_DURATION都會被替換。要想解決此問題,應該設法利用編譯器的某些特性才對。有個辦法比用預處理指令來定義常量更好。下面這行代碼就定義了一個類型為NSTimerInterval的常量:

static const NSTimeInterval kAnimationDuration = 0.3;

此方式定義的常量包含類型信息,其好處是清楚的描述了常量的含義。常量名稱,常用的命名法是:若常量局限於某「編譯單元,也就是實現文件」之內,則在前面加字母k;若常量在類之外可見,則通常以類名為前綴。定義常量的位置很重要。我們總喜歡在頭文件里聲明預處理指令,這樣做很糟糕,當常量名稱有可能互相衝突時更是如此。例如,ANIMATION_DURATION這個常量名就不該用在頭文件中,因為所有引入了這份頭文件的其他文件都會出現這個名字。其實就連用static const定義的那個常量也不應該出現在頭文件里。因為Objective-C沒有「名稱空間」這一概念,所以那樣做等於聲明了一個名叫kAnimationDuration的全局變數。此名稱應該加上前綴,以表明其所屬的類。例如,可改為EOCViewClassAnimationDuration。

2.若不打算公開某個常量,則應將其定義在使用該變數的實現文件里。比方說,要開發一個使用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:^(){

}];

}

@end

變數一定要同時用static與const來聲明。如果試圖修改由const修飾符所聲明的變數,那麼編譯器就會報錯。而static修飾符意味著該變數僅在定義此變數的編譯單元中可見。編譯器每收到一個編譯單元,就會輸出一份目標文件(object file)。在Objective-C的語境下,「編譯單元」一次通常指每個類的實現文件。因此,在上述範例代碼中聲明的kAnimationDuration變數,其作用域僅限於由EOCAnimatedView.m所生成的目標文件中。假如聲明此變數時不加static,則編譯器會為它創建一個「外部符號」,此時若是另一個編譯單元中也聲明了同名變數,那麼編譯器就拋出一條錯誤消息:

duplicate symbol _kAnimationDuration in:

EOCAnimatedView.o

EOCOtherView.o

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

3.有時候需要對外公開某個常量。比方說,你可能要在類代碼中調用NSNotificationCenter以通知他人。用一個對象來派發通知,令其他欲接收通知的對象向該對象註冊,這樣能實現此功能了。派發通知時,需要使用字元串來標識此項通知的名稱,而這個名字就可以聲明為一個外界可見的常值變數,這樣的話,註冊者無須知道實際字元串值,只需以常量變數來註冊自己想要接收的通知即可。此類常量需放在」全局符號表「中,以便可以在定義該常量的編譯單元之外使用。因此,定義方式與之前不同:

//In the header file

extern NSString *const EOCStringConstant;

//In the implementation file

NSString *const EOCStringConstant = @"VALUE";

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

編譯器看到頭文件中的extern關鍵字,就能明白如何在引入此頭文件的代碼中處理該常量了。這個關鍵字是要告訴編譯器,在全局符號表中將會有一個名叫EOCStringConstant的符號。也就是說,編譯器無需查看其定義,即允許代碼使用此常量。因為它知道,當鏈接成二進位文件之後,肯定能找到這個常量。此類常量必須要定義,而且只能定義一次。通常將其定義在與聲明該常量的頭文件相關的實現文件里。由實現文件生成目標文件時,編譯器會在」數據段「為字元串分配存儲空間。鏈接器會把此目標文件與其他目標文件相連接,以生成最終的二進位文件。凡是用到EOCStringConstant這個全局符號的地方,鏈接器都能將其解析。因為符號要放到全局符號表裡,所以命名常量需謹慎。例如,某應用程序中有個處理登錄操作的類,在登錄完成後會發出通知。派發通知所用的代碼如下:

//EOCLoginManager.h

#import <Foundation/Foundation.h>

extern NSString *const EOCLoginManagerDidLoginNotification;

@interface EOCLoginManager : NSObject

- (void)login;

@end

//EOCLoginManager.m

#import "EOCLoginManager.h"

NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

@implementation EOCLoginManager

- (void)login{

}

- (void)p_didLogin{

[[NSNotificationCenter defaultCenter]postNotificationName:EOCLoginManagerDidLoginNotification object:nil];

}

@end

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

其他類型的常量也是如此。例如前面的EOCAnimatedView類里的動畫播放時長對外公布,那麼可以聲明:

//EOCAnimatedView.h

extern const NSTimeInterval EOCAnimatedViewAnimationDuration;

//EOCAnimatedView.m

const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3;

這樣優於使用#define預處理指令,預處理指令所定義的常量可能會無意遭人修改。這種編譯器會確保常量值不變。

*****要點:

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

在實現文件中使用static const來定義」只在編譯單元內可見的常量「,由於此類常量不在全局符號表中,所以無須為其名稱加前綴。

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


推薦閱讀:

「Pin」這款 App 是如何做到不上傳用戶剪貼板內容並能夠推送剪貼板內容通知的?
iOS 11 GM 版固件泄露,新 iPhone 更多細節提前被曝光
iOS 的 Alert View 與 Action Sheet 有什麼區別?
寫程序要悲觀一點

TAG:iOS |