19.使用清晰而協調的命名方式
20.為私有方法名加前綴
21.理解Objective-C錯誤模型
22.理解NSCopying協議
類、方法及變量的命名是Objective-C編程的重要環節。其語法結構使得代碼讀起來和句子一樣。名稱中一般都帶有in、for、with等介詞,而其他編程語言則很少使用這些它們認為多余的字眼:
// C++ string text = "The quick brown fox jumped over the lazy dog"; string newText = text.replace("fox","cat"); // Objective-C NSString *text = @"The quick brown fox jumped over the lazy dog"; NSString *newText = [text stringByReplacingOccurrencesOfString:@"fox" withString:@"cat"];
text.replace(“fox”,”cat”)從字面上無法判斷是用fox替換cat還是用cat替換fox,而Objective-C的命名方式雖然長一點,但卻非常清晰。
C++或Java更習慣簡省的函數名,在這種命名方式下,若想知道每個參數的用途,就得查看函數原型,這會令代碼難於讀懂,以一個矩形的類為例:
// C++ class Rectangle{ public: Rectangle(float width, float height); float getWidth(); float getHeight(); private: float width; float height; }; // Objective-C @interface EOCRectangle : NSObject @property (nonatomic, assign, readonly) float width; @property (nonatomic, assign, readonly) float height; - (id)initWithWidth:(float)width andHeight:(float)height; @end
當創建該類實例時:
// C++ Rectangle aRectangle = new Rectangle(5.0f,10.0f); // Objective-C Rectangle *aRectangle = [[Rectangle alloc]initWithWidth:5.0f andHeight:10.0f];
C++代碼無法判斷5.0f和10.0f分別表示什麼,就算能推測出來表示矩形的尺寸,也不知道寬度在先還是高度在先,需要查函數定義才能確定。而Objective-C的代碼卻很清晰。
雖說長名字可伶代碼更為易讀,但也不能長得太過分了,應盡量言簡意赅:
// 好的方法名 - (EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle - (float)area // 不好的方法名 - (EOCRectangle*)union:(EOCRectangle*)rectangle // 不清晰 - (float)calculateTheArea // 太冗余
給方法命名時的注意事項可以總結為:
如果方法的返回值是新創建的,那麼方法名的首個詞應是返回值類型,除非前面還有修飾語。屬性的存取方式不遵循這種命名方式。 應該把表示參數類型的名詞放在參數前面。 如果方法要在當前對象上執行操作,那麼就應該包含動詞;若執行操作時還需要參數,則應該在動詞後面加上一個或多個名詞。 不要使用str這種簡稱,應該使用string這樣的全稱。 返回Bool值的方法應加上has或is前綴。 get這個前綴留給那些借由輸出參數來保存返回值的方法。Objective-C語言沒辦法將方法標為私有,每個對象都可以響應任意消息。需要開發者在命名慣例中體現私有方法等語義。筆者喜歡用p_作為前綴,p表示private,而下劃線可以把這個字母和真正的方法名區隔開。
#import@interface EOCObject : NSObject - (void)publicMethod; @end @implementation EOCObject // 公有方法 - (void)publicMethod{ // code } // 私有方法 - (void)p_privateMethod{ // code } @end
蘋果公司喜歡單用一個下劃線作私有方法的前綴,所以請不要照蘋果公司的辦法來做,不然有可能無意間重寫父類的同名方法。
#import@interface EOCViewController : UIViewController @end @implementation EOCViewController - (void)_resetViewController{ // code } @end
以上代碼看起來沒有問題,但UIViewController類本身已經實現了一個名叫_resetViewController的方法。這樣寫的話所有調用都將執行子類中的這個方法。由於超類中的同名方法並未對外公布,除非深入研究這個庫,否則根本不會察覺無意間重寫了這個方法。
很多編程語言都有異常(exception)機制,Objective-C也不例外,但需要注意的是,ARC在默認情況下不是異常安全的。這意味著:如果拋出異常,那麼本應在作用域末尾釋放的對象現在卻不會自動釋放了。想要生成異常安全的代碼,需要打開編譯器標志-fobjc-arc-exceptions。
Objecti-C現在所采用的方法是:只在極其罕見的情況下拋出異常,異常拋出之後,無需考慮恢復問題,應用程序直接退出。異常只用於處理嚴重錯誤,出現一般錯誤時,令方法返回nil/0,或使用NSError。
比如初始化無法根據傳入的參數來初始化當前實例,那麼就令其返回nil/0。
- (id)initWithValue:(id)value{ if((self = [super init])){ if(/* 無法用value初始化實例 */){ self = nil; }else{ // 初始化 } } return self; }
而NSError的用法更加靈活,可以把導致錯誤的原因回報給調用者。NSError對象裡封裝了三條信息:Error domain(錯誤范圍,類型為字符串)、Error code(錯誤碼,類型為整數)、User info(用戶信息,類型為字典)。
// EOCErrors.h #import// 聲明全局變量錯誤范圍 extern NSString *const EOCErrorDomain; // 錯誤碼 typedef NS_ENUM(NSUInteger, EOCError){ EOCErrorUnkown = -1, EOCErrorInteralInconsistency = 100, EOCErrorGeneralFault = 105, EOCErrorBadInput = 500, }; @interface EOCErrors : NSObject - (BOOL)doSomething:(NSError**)error; @end // EOCErrors.m #import "EOCErrors.h" NSString *const EOCErrorDomain = @"EOCErrorDomain"; @implementation EOCErrors - (BOOL)doSomething:(NSError *__autoreleasing *)error{ if(/* error發生 */){ if (error) { *error = [NSError errorWithDomain:EOCErrorDomain code:EOCErrorGeneralFault userInfo:@{@"EOCErrorUserInfo":@"General fault occurs!"}]; } return NO; } return YES; } @end
這樣就能經由輸出參數把NSError對象回傳給調用者:
NSError *error = nil; BOOL ret = [object doSomething:&error]; if (error){ // code }
使用對象時,經常需要拷貝它。在Objective-C中,此操作通過copy方法完成。如果想令自己的類支持拷貝操作,那就要實現NSCopying協議,該協議只有一個方法:
- (id)copyWithZone:(NSZone*)zone
現在每個程序只有一個區了(默認區),所以實現這個方法時不用擔心其中的zone參數。copy方法由NSObject實現,該方法只是以默認區為參數來調用copyWithZone:方法。所以我們想的是重寫copy方法,真正需要實現的是copyWithZone:方法。
下面是一個實現NSCopying協議的例子:
// EOCPerson.h #import@interface EOCPerson : NSObject @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName; - (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 *_friends; } - (void)addFriend:(EOCPerson *)person{ [_friends addObject:person]; } - (void)removeFriend:(EOCPerson *)person{ [_friends removeObject:person]; } - (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{ if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _friends = [NSMutableSet new]; } return self; } // 實現NSCopying協議 - (id)copyWithZone:(NSZone *)zone{ // 以全能初始化方法初始化拷貝對象 EOCPerson *copy = [[[self class]allocWithZone:zone]initWithFirstName:_firstName lastName:_lastName]; // 將實例變量拷貝到拷貝對象 copy->_friends = [_friends mutableCopy]; return copy; } @end
本例中,_friends是使用mutableCopy方法來復制的,此方法來自另一個叫做NSMutableCopying的協議,與NSCopying類似,也只定義了一個方法:
- (id)mutableCopyWithZone:(NSZone*)zone
如果需要返回不可變的拷貝,則應該實現NSCopying協議,而若需要返回可變的拷貝,則應實現NSMutableCopying協議:
[NSMutableArray copy] // 返回NSArray [NSArray mutableCopy] // 返回NSMutableArray
編寫拷貝方法時,還要決定一個問題,就是應該執行深拷貝還是淺拷貝。深拷貝的意思就是:在拷貝對象自身時,將底層數據也一並復制過去。而淺拷貝只拷貝容器本身,而不復制其中數據。Foundation框架中的所有容器在默認情況下都執行淺拷貝。
在EOCPerson那個例子中,若需要深拷貝的話,可以編寫一個專供深拷貝的方法:
- (id)deepCopy{ EOCPerson *copy = [[[self class]alloc]initWithFirstName:_firstName lastName:_lastName]; // copyItem參數設為YES,則會向容器中每個元素都發送copy消息,用拷貝好的元素創建新set返回給調用者。 copy->_friends = [[NSMutableSet alloc]initWithSet:_friends copyItem:YES]; return copy; }