這是Objective-C系列的第3篇。編程
爲全部類名都加上適當的前綴數組
提供"全能初始化方法"安全
description方法bash
儘可能建立不可變對象;session
readonly
屬性擴展爲readwrite
;命名框架
參考使用清晰而協調的命名方式dom
copy函數
趕上以下錯誤嗎?post
duplicate symbol _OBJC_METACLASS_$DuplicateTheClass in
build /somethings.o
build /something_else.o
複製代碼
以上錯誤是因爲在應用程序中兩份代碼都各類實現了DuplicateTheClass
的類,致使該類符號和元類各定義了兩次。ui
避免此問題的惟一辦法就是變相實現命名空間:爲全部名稱都加上適當前綴。
前綴能夠是公司、應用程序或者功能模塊,甚至是我的標識等關聯的詞彙。但要注意的是,Apple宣稱其保留全部使用「兩字母前綴」的權利,因此你本身選用的前綴應該是三個字母的。
還有一個要注意的問題是,類的實現文件中所用的純C函數以及全局變量,在編譯好的目標文件中,這些都是「頂級符號(top-level symbol)」。
同時,也要注意在引入第三方庫的時候,注意別使得本身的命名與其餘庫命名重合。
設計類的時候,應充分使用屬性來封裝數據。而在使用屬性時,則可將其聲明爲readonly
。默認的屬性都是readwrite
。
在設計類的時候,將某些不須要改變的屬性設置爲readonly
,以防止被改動。
@interface HOReadOnlyProperty : NSObject
@property (nonatomic ,readonly ,copy)NSString *name;
@end
複製代碼
在這裏,雖然指定了readonly
,就沒有設置方法,但仍是指定了內存管理語義,是爲了之後方便將其改動爲可讀寫屬性。
有時可能想修改封裝在對象內部的數據,可是卻不想令這些數據爲外人所改動。一般是在對象內部將readonly
屬性從新聲明爲readwrite
。以下:
@interface HOReadOnlyProperty()
@property (nonatomic ,readwrite ,copy)NSString *name;
@end
@implementation HOReadOnlyProperty
@end
複製代碼
這樣就能夠實現只在內部改變屬性值了。
!可是!並不能保證外部不會改變屬性值,由於仍然能夠經過KVC實現改變。經過setValue:forKey:
來修改。
另外,在定義表示各類collection的那些屬性時,通常會提供一個readonly
屬性供外界使用,該屬性返回一個不可變的set,而此set是內部那個可變set的一個拷貝。好比下面:
@interface HOPerson : NSObject
@property (nonatomic ,copy)NSString *name;
@property (nonatomic ,strong ,readonly)NSSet *friends;
@end
複製代碼
實現文件:
@interface HOPerson()
{
NSMutableSet *_internalFriends;
}
@end
@implementation HOPerson
- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
_name = name;
_internalFriends = [NSMutableSet set];
}
return self;
}
- (NSSet *)friends
{
return [_internalFriends copy];
}
- (void)addFriend:(HOPerson *)person
{
[_internalFriends addObject:person];
}
- (void)removeFriend:(HOPerson *)person
{
[_internalFriends removeObject:person];
}
@end
複製代碼
readonly
屬性擴展爲readwrite
;stringWithString
。除非前面有修飾語,好比localizedString
。屬性的存取方法不遵循這種命名方式,由於通常認爲這些方法不會建立新對象,即使有時返回內部對象的一份拷貝,咱們也認爲那至關於原有的對象。這些存取方法應該按照其所對應的屬性來命名;-(void)getCharacters:(unichar*)buffer range:(NSRange)aRange
中的range
表述的參數類型;lowercaseString
;str
這種簡稱,應該使用string
這樣的全稱;is
前綴。若是某方法返回非屬性的Boolean值,那麼應該根據其功能,選用has
或is
爲前綴;應該爲類與協議的名稱加上前綴,以免命名空間衝突。同時添加View
,ViewController
,Delegate
,Protocol
等這樣標識類與協議的名詞。
一個類所作的事情一般都要比從外面看到的更多。編寫類的實現代碼時,常常要編寫一些只在內部使用的方法。應該爲這中方法加上某些前綴,這有助於調試,由於據此很容易就能把公共方法和私有方法區分開。
另外,前綴的存在便於修改方法名或方法簽名,即這些有前綴的方法能夠隨時修改,而不用擔憂會影響面向外界的那些API。
具體使用何種前綴可根據我的喜愛而定,可是最好包括下劃線與字母p。p即private,下劃線是爲了與真正的方法名區分開。因此私有方法名會寫成:
- (void)p_privateMethod {
/.../
}
複製代碼
給私有方法的名稱加上前綴,這樣能夠很容易地將其同公共方法區分開;
不要單用一個下劃線前綴,由於這種方法是預留給蘋果公司用的。
Objective-C 也有異常機制,可是在默認狀況下不是「異常安全的(exception safe)」。這意味着:若是拋出異常,那麼本應在做用於末尾釋放的對象選擇卻不會自動釋放了。
若是想生成「異常安全」的代碼,能夠經過設置編譯標誌來實現,不過這將引入一些額外的代碼,在不拋出異常時,也照樣要執行這部分代碼。須要打開的編譯器標誌是:-fobjc-arc-exceptions。
上面說的是ARC的情形,那麼在MRC呢?
即便MRC時,也很難寫出拋出異常不會致使內存泄漏的代碼。好比,先建立好某資源,使用完以後再將其釋放,可是在釋放以前若是拋出異常,這部分資源就不會被釋放了。這種問題的解決方案之一,就是在拋出異常前先釋放資源。這樣的確能解決問題,不過萬一資源過多,執行路徑複雜,那麼在拋出什麼異常時該釋放哪些資源就會雜亂無章。
針對上面的問題,Objective-C的解決辦法是:只在極其罕見的狀況下拋出異常,異常拋出以後,無須考慮恢復問題,並且應用程序也應該退出。也就是說,無須編寫複雜的「異常安全」的代碼了。
異常只應該應用於極其嚴重的錯誤。
對於那些「不那麼嚴重的錯誤(nonfatal error,非致命錯誤)」,Objective-C所用的編程範式:令方法返回nil/0,或是使用NSError,以代表有錯誤發生。
好比,在init時,返回nil,意指初始化失敗。
NSError的用法靈活,經由此對象,能夠把致使錯誤的緣由回報給調用者。
-(instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;
複製代碼
Error domain(錯誤範圍,其類型爲字符串)
即錯誤產生的根源,一般用一個特有的全局變量來定義。系統提供了一些,參考NSError.h頭文件。
Error code(錯誤碼,其類型爲整數)
User info(用戶信息,其類型爲字典)
那麼如何將錯誤告知調用者,即經過什麼途徑傳遞出去?
第一種常見用法是經過委託協議來傳遞此錯誤。
- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
複製代碼
另一種常見用法是:經由方法的「輸出參數」返回給調用者。
- (BOOL)doSomething:(NSError **)error
{
if(/*此處是錯誤邏輯*/){
if(error) {
//必須先判斷error,假如nil時,解引用會致使「段錯誤(segmentation fault)」,致使程序崩潰
*error = [NSError errorWithDomain:domain code:code userinfo:userinfo];
}
return NO;
} else {
return YES;
}
}
//調用
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
//處理錯誤
}
複製代碼
下面示例了一個domain以及code的代碼:
//HOErrors.h
extern NSString *const HOErrorDomain;
typedef NS_ENUM(NSUInteger, HOError) {
HOErrorUnknown = -1,
HOErrorInternalInconsistency = 100,
}
//HOErrors.m
NSString *const HOErrorDomain = @"HOErrorDomain"
複製代碼
使用對象時,常常須要拷貝。在Objective-C中,此操做經過copy方法完成。若是要想令某個類支持copy,須要實現NSCopying協議,該協議只有一個方法:
- (id)copyWithZone:(NSZone*)zone;
複製代碼
其中,zone在以前開發中會據此把內存分紅不一樣的「區(zone)」,而對象建立在某個區裏面。如今不用了,每一個程序只有一個區:默認區(default zone)。因此,不用管zone這個參數。
有時候,須要實現可變版本的拷貝,遵循NSMutableCopying協議,實現:
- (id)mutableCopyWithZone:(NSZone*)zone;
複製代碼
在編寫拷貝方法是,還要以爲是執行「深拷貝(deep copy)」仍是「淺拷貝(shallow copy)」。深拷貝時,在拷貝對象自身是,將其底層數據也一併複製過去。Foundation框架中全部的collection類在默認狀況下都執行淺拷貝,即只拷貝對象自己,而不復制其中數據。
通常狀況下,咱們在實現拷貝時,遵循系統所使用的那種樣式,即以淺拷貝實現「copyWithZone」方法。可是若是有必要的話,增長一個執行深拷貝的方法。以NSSet爲例,該類實現深拷貝的方法:
- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;
複製代碼
若copyItems爲YES,則該方法會向數組中的每一個元素都發送copy消息,用拷貝好的元素建立新的set,並返回給調用者。