15.用前綴避免命名空間沖突
16.提供全能化初始方法
17.實現description方法
18.盡量使用不可變對象
Objecti-C沒有其他語言那種內置的命名空間機制,鑒於此,我們在起名時要設法避免潛在的命名沖突,否則很容易出現重名。
避免此問題的唯一辦法就是變相實現命名空間:為所有名稱都加上適當前綴。所選前綴可以是與公司、應用程序或者二者皆有關聯之名。比方說,假設你所在的公司叫做Effective Widgets,那麼就可以在所有應用程序都會用到的那部分代碼中使用EWS作前綴,如果有些代碼只用於名為Effective Browser的浏覽器項目中,那這部分可以使用EWB作前綴。
使用Cocoa創建應用程序時一定要注意,Apple宣稱其保留使用所有兩字母前綴的權利,所有你自己選擇用的前綴應該是三個字母的。不然可能會與框架中的類名發生沖突。
不僅是類名,應用程序中的所有名稱都應加前綴,包括分類。還有一個容易引發命名沖突的地方,那就是類的實現文件中所有的純C函數及全局變量。
// EOCSoundPlayer.m #import "EOCSoundPlayer.h" #import void completion(SystemSoundID ssID, void *clientData){ // code } @implementation EOCSoundPlayer // code @end
這段代碼看起來完全正常,但在該類目標文件中的符號表中可以看到一個名叫_completion的符號,這就是completion函數。雖說此函數是在實現文件裡定義的,並沒有聲明在頭文件中,不過它仍算作頂級符號。如果別處又創建一個名叫completion的函數,則會發生重復符號錯誤。由此可見,應該總是給這種C類型函數的名字加上前綴。比如可以改名為EOCSoundPlayerCompletion。
如果用第三方庫編寫自己的代碼,這時應該給你所用的那一份第三方庫代碼都加上你自己的前綴。
所有對象均要初始化,在初始化時,有些對象可能無須開發者向其提供額外信息。這種可為對象提供必要信息以便其能完成工作的初始化方法叫做全能初始化方法。
如果創建類實例的方法有多種,仍然需要在其中選定一個全能的初始化方法,令其他初始化方法都來調用它。NSDate就是個例子,其初始化方法如下:
- (id)init; - (id)initWithTimeIntervalSinceNow:(NSTimeInterval)secs; - (id)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date; - (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti; - (id)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
在上面幾個方法中initWithTimeIntervalSinceReferenceDate:是全能初始化方法。其余的初始化方法都要調用它。於是,只有在全能初始化方法中才會存儲內部數據。
比如,要編寫一個表示矩形的類。可以這樣寫:
// EOCRectangle.h #import@interface EOCRectangle : NSObject @property (nonatomic, assign, readonly) float width; @property (nonatomic, assign, readonly) float height; // 全能初始化方法 - (id)initWithWidth:(float)width andHeight:(float)height; @end // EOCRectangle.m #import "EOCRectangle.h" @implementation EOCRectangle -(id)initWithWidth:(float)width andHeight:(float)height{ if ((self = [super init])) { _width = width; _height = height; } return self; } @end
可是,如果用[[EOCRectangle alloc] init]方法來創建矩形,會調用EOCRectangle超類NSObject實現的init方法,調用完該方法後,全部實例變量都將設為0,所以應該使用下面兩種版本中的一種來重寫init方法:
// 設置默認值調用全能初始化方法 - (id)init{ return [self initWithWidth:5.0 andHeight:10.0]; } // 拋出異常 - (id)init{ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil]; }
再創建一個EOCSquare類,為EOCRectangle類的子類,專門用來表示正方形,寬度與高度必須相等。這時候需要注意的是,全能初始化方法的調用鏈一定要維系,如果超類的初始化方法不適用於子類,那麼應該重寫這個超類方法。子類的全能初始化方法與超類的名稱不同時,應該重寫超類的全能初始化方法。
// EOCSquare.h #import "EOCRectangle.h" @interface EOCSquare : EOCRectangle // 子類的全能初始化方法 - (id)initWithDemension:(float)dimension; @end // EOCSquare.m #import "EOCSquare.h" @implementation EOCSquare - (id)initWithDemension:(float)dimension{ return [super initWithWidth:dimension andHeight:dimension]; } // 利用子類的全能初始化方法重寫超類的全能初始化方法 - (id)initWithWidth:(float)width andHeight:(float)height{ float dimension = MAX(width, height); return [self initWithDemension:dimension]; } @end
不需要再在子類中重寫init方法了,當用[[EOCSquare alloc] init]創建對象時,會調用超類EOCRectangle的init方法,超類的init方法已經重寫,會拋出異常或者調用initWithWidth:andHeight:方法,由於子類重寫了該方法,所以執行的是子類的該方法,而子類的該方法又會調用子類的全能初始化方法。
調試程序時,經常需要打印並查看對象信息。一種辦法是編寫代碼把對象的全部屬性都輸出到日志中。不過常用的做法還是像下面這樣:
NSArray *object = @[@"A string", @(123)]; NSLog(@"object = %@", object);
則會輸出:
object = ( "A string", 123 )
如果在自定義類上這麼做的話,輸出結果會是這樣:
object =
這樣的信息不太有用,如果沒有在自己的類裡重寫description方法,就會調用NSObject類所實現的默認方法。此方法定義NSObject協議裡,只輸出類名和對象的內存地址。想輸出更多有用信息,就需要重寫description方法:
#import@interface EOCPerson : NSObject @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName; @end @implementation EOCPerson - (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{ if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; } return self; } // 實現description方法 - (NSString*)description{ return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",[self class], self, _firstName, _lastName]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"]; NSLog(@"person = %@", person); } return 0; }
運行結果:
2016-07-25 17:31:35.376 OCTest[41472:1864742] person =
也可以用字典的格式來打印對象屬性:
- (NSString*)description{ return [NSString stringWithFormat:@"<%@: %p, %@>",[self class], self, @{@"firstName":_firstName,@"lastName":_lastName}]; }
運行結果:
2016-07-25 17:37:16.000 OCTest[41801:1868356] person =
在NSObject協議中還有一個與description方法非常類似的方法debugDescription。debugDescription方法是開發者在調試器中以控制台命令打印對象時才調用的。在NSObject類的默認實現中,此方法只是直接調用了description。
如果我們只想在description方法中描述對象的普通信息,而將更詳盡的內容放在調試所用的描述信息裡,此時可用下列代碼實現這兩個方法:
- (NSString*)description{ return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName]; } - (NSString*)debugDescription{ return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",[self class], self, _firstName, _lastName]; }
在代碼中插入斷點:
int main(int argc, const char * argv[]) { @autoreleasepool { EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"]; NSLog(@"person = %@", person); // 在本行插入斷點 } return 0; }
當程序運行到斷點時,在調試控制台輸入命令。LLDB的po命令可以完成對象打印工作,這時候在控制台輸入po person,運行結果如下:
2016-07-25 17:43:54.427 OCTest[42181:1872419] person = Bob Smith (lldb) po person
默認情況下,屬性是“既可讀又可寫的”(readwrite),這樣設計出來的類都是可變的。不過一般情況下我們要建模的數據未必是需要改變的。
有時可能想修改封裝在對象內部的數據,但是卻不想令這些數據為外人所改動。這種情況下通常做法是在對象內部將readonly屬性重新聲明為readwrite。這一操作可於class-continuation分類(擴展)中完成。另外,對象裡表示各種容器的屬性也可以設為不可變的,通過下面的方法來使類的用戶操作此屬性:
// EOCPerson.h #import@interface EOCPerson : NSObject // 所有屬性都聲明為只讀的 @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; @property (nonatomic, strong, readonly) NSSet *friends; - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName; // 提供給類的用戶操作friends屬性的方法 - (void)addFriend:(EOCPerson*)person; - (void)removeFriend:(EOCPerson*)person; @end // EOCPerson.m #import "EOCPerson.h" @interface EOCPerson() // 在擴展中重新聲明屬性為可讀寫的,在對象內部可以操作屬性 @property (nonatomic, copy, readwrite) NSString *firstName; @property (nonatomic, copy, readwrite) NSString *lastName; @end @implementation EOCPerson{ NSMutableSet *_internalFriends; } // 重寫friends的getter方法 - (NSSet*)friends{ return [_internalFriends copy]; } // 向friends中添加對象 - (void)addFriend:(EOCPerson *)person{ [_internalFriends addObject:person]; } // 從friends中移除對象 - (void)removeFriend:(EOCPerson *)person{ [_internalFriends removeObject:person]; } - (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{ if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _internalFriends = [NSMutableSet new]; } return self; } @end
如果用NSMutableSet來實現friends屬性的話,可以不用addFriend:與removeFriend:方法就能直接操作此屬性。但是這樣做很容易出bug,采用這種做法,就等於直接從底層修改了其內部用於存放朋友對象的set。而EOCPerson毫不知情,可能會令對象內的各數據不一致。
另外需要注意的是,用擴展的方式將屬性從readonly改為readwrite來實現屬性僅供對象內部修改的做法也有一些漏洞。在對象外部,仍然可以通過鍵值編碼(KVC)來修改這些屬性。例如:
[person setValue:@"Seven" forKey:@"firstName"]
或者不通過setter方法,直接用類型信息查詢功能查出屬性所對應的實例變量在內存布局中的偏移量,以此來認為設置這個實例變量的值。
不過這兩種做法都是不合規范的,不應該因為這個原因就放棄盡量編寫不可變對象的做法。