Objective-C(三)接口與API設計

這是Objective-C系列的第3篇。編程

1、最佳實踐

  • 爲全部類名都加上適當的前綴數組

  • 提供"全能初始化方法"安全

    • 在類中提供一個全能初始化方法,並與文檔中指明。其餘初始化方法均應調用此方法;
    • 若全能初始化方法與超類方法不一樣,則須要覆寫超類中對應的方法;
    • 若是超類方法的初始化方法不適用於子類,那麼應該覆寫這個超類方法,並在其中拋出異常。
  • description方法bash

    • 實現description方法返回一個有意義的字符串,用於描述該實例;
    • 若想在調試時,打印出更詳盡的對象描述信息,則應實現debugDescription方法。
    • 能夠去尋找實現這樣的插件來快速生成該方法。
  • 儘可能建立不可變對象;session

    • 若屬性僅可用於對象內部修改,則在「class-continuation分類」中將其有readonly屬性擴展爲readwrite
    • 不要把可變的collection做爲屬性公開,而應提供相關方法,以此修改對象中可變的collection。
  • 命名框架

    參考使用清晰而協調的命名方式dom

  • copy函數

    • 若想令本身的所寫的對象具備拷貝功能,則需實現NSCopying協議。
    • 若是自定義對象分爲可變版本和不可變版本,那麼就要同時實現NSCopying與NSMutableCopying。
    • 複製對象時需決定採用淺拷貝仍是深拷貝,通常狀況下應該儘可能執行淺拷貝。
    • 若是你所寫的對象須要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。

2、實踐詳解

2.1 用前綴避免命名空間的衝突

趕上以下錯誤嗎?post

duplicate symbol _OBJC_METACLASS_$DuplicateTheClass in 
 	build /somethings.o
 	build /something_else.o
複製代碼

以上錯誤是因爲在應用程序中兩份代碼都各類實現了DuplicateTheClass的類,致使該類符號和元類各定義了兩次。ui

避免此問題的惟一辦法就是變相實現命名空間:爲全部名稱都加上適當前綴

前綴能夠是公司、應用程序或者功能模塊,甚至是我的標識等關聯的詞彙。但要注意的是,Apple宣稱其保留全部使用「兩字母前綴」的權利,因此你本身選用的前綴應該是三個字母的。

還有一個要注意的問題是,類的實現文件中所用的純C函數以及全局變量,在編譯好的目標文件中,這些都是「頂級符號(top-level symbol)」。

同時,也要注意在引入第三方庫的時候,注意別使得本身的命名與其餘庫命名重合。

2.2 提供「全能初始化方法」

  • 在類中提供一個全能初始化方法,並與文檔中指明。其餘初始化方法均應調用此方法;
  • 若全能初始化方法與超類方法不一樣,則須要覆寫超類中對應的方法;
  • 若是超類方法的初始化方法不適用於子類,那麼應該覆寫這個超類方法,並在其中拋出異常。

2.3 實現description方法

  • 實現description方法返回一個有意義的字符串,用於描述該實例;
  • 若想在調試時,打印出更詳盡的對象描述信息,則應實現debugDescription方法。
  • 能夠去尋找實現這樣的插件來快速生成該方法。

2.4 儘可能使用不可變對象

設計類的時候,應充分使用屬性來封裝數據。而在使用屬性時,則可將其聲明爲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
複製代碼
  • 儘可能建立不可變對象;
  • 若屬性僅可用於對象內部修改,則在「class-continuation分類」中將其有readonly屬性擴展爲readwrite
  • 不要把可變的collection做爲屬性公開,而應提供相關方法,以此修改對象中可變的collection。

2.5 使用清晰而協調的命名方式

2.5.1 方法命名規則

  • 若是方法的返回值是新建立的,那麼方法名的首個詞應是返回值的類型,好比stringWithString。除非前面有修飾語,好比localizedString。屬性的存取方法不遵循這種命名方式,由於通常認爲這些方法不會建立新對象,即使有時返回內部對象的一份拷貝,咱們也認爲那至關於原有的對象。這些存取方法應該按照其所對應的屬性來命名;
  • 應該把表述參數類型的名詞放在參數前面。好比-(void)getCharacters:(unichar*)buffer range:(NSRange)aRange中的range表述的參數類型;
  • 若是方法要在當前對象執行操做,那麼久應該包含動詞;若執行操做還須要參數,則應該在動詞後面再加上一個或多個名詞,好比:lowercaseString
  • 不要使用str這種簡稱,應該使用string這樣的全稱;
  • Boolean屬性應該加上is前綴。若是某方法返回非屬性的Boolean值,那麼應該根據其功能,選用hasis爲前綴;

2.5.2 類與協議的命名

應該爲類與協議的名稱加上前綴,以免命名空間衝突。同時添加ViewViewControllerDelegateProtocol等這樣標識類與協議的名詞。

  • 方法名要言簡意賅,從左到右讀起來要像個平常用語中的句子纔好;
  • 方法名裏不要用縮略後的類型名稱;
  • 給方法起名的第一要務就是要確保其風格與你本身的代碼或所要集成的框架相符。

2.5.3 爲私有方法名加前綴

一個類所作的事情一般都要比從外面看到的更多。編寫類的實現代碼時,常常要編寫一些只在內部使用的方法。應該爲這中方法加上某些前綴,這有助於調試,由於據此很容易就能把公共方法和私有方法區分開。

另外,前綴的存在便於修改方法名或方法簽名,即這些有前綴的方法能夠隨時修改,而不用擔憂會影響面向外界的那些API。

具體使用何種前綴可根據我的喜愛而定,可是最好包括下劃線與字母p。p即private,下劃線是爲了與真正的方法名區分開。因此私有方法名會寫成:

- (void)p_privateMethod {
		/.../	
}
複製代碼
  • 給私有方法的名稱加上前綴,這樣能夠很容易地將其同公共方法區分開;

  • 不要單用一個下劃線前綴,由於這種方法是預留給蘋果公司用的。

2.6 理解Objective-C錯誤模型

​ 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"
複製代碼

2.7 理解NSCopying協議

​ 使用對象時,常常須要拷貝。在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,並返回給調用者。

  • 若想令本身的所寫的對象具備拷貝功能,則需實現NSCopying協議。
  • 若是自定義對象分爲可變版本和不可變版本,那麼就要同時實現NSCopying與NSMutableCopying。
  • 複製對象時需決定採用淺拷貝仍是深拷貝,通常狀況下應該儘可能執行淺拷貝。
  • 若是你所寫的對象須要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。
相關文章
相關標籤/搜索