標籤:objective
與C 和C++ 一樣,Objective-C 也使用“標頭檔”(header file) 與“實現檔案”(implementation file)來區隔代碼。用Objective-C 語言編寫“類”(class)的標準方式為:以類名做檔案名稱,分別建立兩個檔案,標頭檔尾碼用.h,實現檔案尾碼用.m。建立好一個類之後,其代碼看上去如下所示:
// EOCPerson.h#import <Foundation/Foundation.h>@interface EOCPerson : NSObject@property (nonatomic, copy) NSString *firstName;@property (nonatomic, copy) NSString *lastName;@end
// EOCPerson.m#import "EOCPerson.h"@implementation EOCPerson// Implementation of methods@end
用Objective-C 語言編寫任何類幾乎都需要引入Foundation.h。如果不在該類本身引入這個檔案的話,那麼就要引入與其超類所屬架構相對應的“基本標頭檔”(base header file)。例如,在建立iOS 應用程式時,通常會繼承UIViewController 類。而這些子類的標頭檔需要引入UIKit.h。現在看來,EOCPerson 類還好。其標頭檔引入了整個Foundation 架構,不過這並沒有問題。如果此類繼承自Foundation 架構中的某個類,那麼EOCPerson 類的使用者(consumer)
可能會用到其基類中的許多內容。繼承自UIViewController 的那些類也是如此,其使用者可能會用到UIKit 中的大部分內容。
過段時間, 你可能又建立了一個名為EOCEmployer 的新類, 然後可能覺得每個EOCPerson 執行個體都應該有一個EOCEmployer。於是,直接為其添加一項屬性:
// EOCPerson.h#import <Foundation/Foundation.h>@interface EOCPerson : NSObject@property (nonatomic, copy) NSString *firstName;@property (nonatomic, copy) NSString *lastName;@property (nonatomic, strong) EOCEmployer *employer;@end
然而這麼做有個問題,就是在編譯引入了EOCPerson.h 的檔案時,EOCEmployer 類並不可見。不便強迫開發人員在引入EOCPerson.h 時必須一併引入EOCEmployer.h,所以,常見的
辦法是在EOCPerson.h 中加入下面這行:
#import "EOCEmployer.h"
這種辦法可行,但是不夠優雅。在編譯一個使用了EOCPerson 類的檔案時,不需要知道
EOCEmployer 類的全部細節,只需要知道有一個類名叫EOCEmployer 就好。所幸有個辦法
能把這一情況告訴編譯器:
@class EOCEmployer;
這叫做“向前聲明”(forward declaring)該類。現在EOCPerson 的標頭檔變成了這樣:
// EOCPerson.h#import <Foundation/Foundation.h>@class EOCEmployer;@interface EOCPerson : NSObject@property (nonatomic, copy) NSString *firstName;@property (nonatomic, copy) NSString *lastName;@property (nonatomic, strong) EOCEmployer *employer;@end
EOCPerson 類的實現檔案則需引入EOCEmployer 類的標頭檔,因為若要使用後者,則
必須知道其所有介面細節。於是,實現檔案就是:
// EOCPerson.m#import "EOCPerson.h"#import "EOCEmployer.h"@implementation EOCPerson// Implementation of methods@end
將引入標頭檔的時機盡量延後,只在確有需要時才引入,這樣就可以減少類的使用者所需引入的標頭檔數量。假設本例把EOCEmployer.h 引入到EOCPerson.h,那麼只要引入EOCPerson.h,就會一併引入EOCEmployer.h 的所有內容。此過程若持續下去,則要引入許
多根本用不到的內容,這當然會增加編譯時間。
向前聲明也解決了兩個類互相引用的問題。假設要為EOCEmployer 類加入新增及刪除僱員的方法,那麼其標頭檔中會加入下述定義:
- (void)addEmployee:(EOCPerson*)person;
- (void)removeEmployee:(EOCPerson*)person;
此時, 若要編譯EOCEmployer, 則編譯器必須知道EOCPerson 這個類, 而要編譯EOCPerson,則又必須知道EOCEmployer。如果在各自標頭檔中引入對方的標頭檔,則會導致“循環參考”(chicken-and-egg situation)。當解析其中一個標頭檔時,編譯器會發現它引入
了另一個標頭檔,而那個標頭檔又回過頭來引用第一個標頭檔。使用#import 而非#include指令雖然不會導致死迴圈,但卻這意味著兩個類裡有一個無法被正確編譯。如果不信的話,讀者可以自己試試。
但是有時候必須要在標頭檔中引入其他標頭檔。如果你寫的類繼承自某個超類,則必須引入定義那個超類的標頭檔。同理,如果要聲明你寫的類遵從某個協議(protocol),那麼該協議必須有完整定義,且不能使用向前聲明。向前聲明只能告訴編譯器有某個協議,而此時
編譯器卻要知道該協議中定義的方法。
例如,要從圖形類中繼承一個矩形類,且令其遵循繪製協議:
// EOCRectangle.h#import "EOCShape.h"#import "EOCDrawable.h"@interface EOCRectangle : EOCShape<EOCDrawable>@property (nonatomic, assign) float width;@property (nonatomic, assign) float height;@end
第二條#import
是難免的。鑒於此,最好是把協議單獨放在一個標頭檔中。要是把EOCDrawable 協議放在了某個大的標頭檔裡,那麼只要引入此協議,就必定會引入那個頭文
件中的全部內容,如此一來,就像上面說的那樣,會產生相互依賴問題,而且還會增加編譯
時間。
然而有些協議,例如“委託協議”(delegate protocol),就不用單獨寫一個
標頭檔了。在那種情況下,協議只有與接受協議委託的類放在一起定義才有意義。此時最好
能在實現檔案中聲明此類實現了該委託協議,並把這段實現代碼放在“ class-continuation 分
類”(class-continuation category)裡。這樣的話,只要在實現檔案中引入包含
委託協議的標頭檔即可,而不需將其放在公用標頭檔(public header file)裡。每次在標頭檔中引入其他標頭檔之前,都要先問問自己這樣做是否確有必要。如果可以用向前聲明取代引入,那麼就不要引入。若因為要實現屬性、執行個體變數或者要遵循協議而必須引入標頭檔,則應盡量將其移至“ class-continuation 分類”中。這樣做不僅可以縮減編譯時間,而且還能降低彼此依賴程度。若是依賴關係過於複雜,則會給維護帶來麻煩,而且,如果只想把代碼的某個部分開放為公用API 的話,太複雜的依賴關係也會出問題。
要點 :
- 除非確有必要,否則不要引入標頭檔。一般來說,應在某個類的標頭檔中使用向前聲
明來提及別的類,並在實現檔案中引入那些類的標頭檔。這樣做可以盡量降低類之間
的耦合(coupling)。
- 有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況下,盡量把“該類遵循某協議”的這條聲明移至“ class-continuation 分類”中。如果不行的話,就把協議單獨放在一個標頭檔中,然後將其引入。
在類的標頭檔中盡量少引入其他標頭檔 <<Effective Objective-C>>