Chapter 3 接口與 API 設計
- Tips 15 使用字首避免明明空間沖突
- Objective-C 沒有命名空間,是以我們在起名時要設法避免命名沖突
- 避免命名沖突的方法就是使用字首
- 應用中的所有名稱都需要加字首(包括實作檔案中的全局變量和純 C 函數)
- Tips 16 提供“全能(designated)初始化方法”
- 一個會被所有初始化方法調用到的初始化方法
- 當底層資料存儲機制變化時,隻需要修改這個方法就可以了,不需要改動其他初始化方法
- 如果超類的全能初始化方法不适用于子類,或是與超類不同,那麼需要覆寫這個超類方法
- 子類的全能初始化方法都應該調用超類的對應方法,逐級向上
- Tips 17 實作 description 方法
- 在數組字典等集合對象列印時,都會調用對象的
方法,友善調試description
- 系統預設的
方法對于自定義的對象并沒有輸出較為有用的内容,是以可以實作這個方法友善我們顯示對象description
- 在調試時會調用
方法(也就是在調試時 lldb 中輸入 po 時調用的将會是debugDescription
),是以實作他可以幫助我們調試時獲得更多的資訊debugDescription
- 可以使用
來實作NSDictionary
方法,這樣顯示和輸出都會比較友善,例如:description
// Header File // 這裡我略微修改了下原書中的示例代碼 @interface EOCLocation : NSObject @property (nonatomic, copy) NSString *title; @property (nonatomic) CGFloat latitude; @property (nonatomic) CGFloat longitude; @end // 我們要是可以使用 NSLog(@"%@", eoc_location) 直接輸出這個對象的經緯度(也就是所有屬性)就好了,那麼可以參考下面的寫法實作 description 方法 @implementation EOCLocation - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, @{ @"title": self.title, @"latitude": @(self.latitude), @"longitude": @(self.longitude), }]; } @end
- 在數組字典等集合對象列印時,都會調用對象的
- Tips 18 盡量使用不可變對象
- 減少 side effect,在使用了一段時間的 RAC 和學習函數式思想後,一定程度上了解了不可變對象的好處
- 具體開發實踐中,應盡量把對外公布的屬性設為隻讀,并且有必要時才對外公布,否則使用私有屬性
- 對于隻讀屬性,可以不用指定記憶體管理語義(也就是 strong,weak,copy)
- 對外隻讀的屬性可以在對象内部,也就是類擴充(Class-Extension 也叫 Class-Continuation)中重新聲明為可讀寫的
- 可以使用 GCD 來設定讀寫操作為同步操作
- 就算屬性設定為隻讀,在外部仍可以使用 KVC 來通路這些屬性,例如:
[object setValue:@"value" forKey:@"propertyName"]
- 集合屬性(Array,Set,Dictionary)可以提供隻讀屬性供外界使用(内部儲存可變類型的變量,傳回該變量的不可變拷貝),并提供操相應的操作方法,例如下面例子中,使用
和-addFriend:
方法來實作對-removeFriend:
集合的操作,這樣保證了添加或删除盆友的操作對象是知情的。對于直接修改friends
集合的操作對象是不知情的,這樣可能會導緻對象内各資料的不一緻。friends
@interface EOCPerson : NSObject @property (nonatomic, strong, readonly) NSSet *friends; @end @implementation EOCPerson { NSMutableSet *_internalFriends; } - (NSSet *)friends { return [_internalFriends copy]; } - (void)addFriend:(EOCPerson *)person { [_internalFriends addObject:person]; } - (void)removeFriend:(EOCPerson *)person { [_internalFriends removeObject:person]; } @end
- 不要在傳回的對象上查詢其是否是可變對象并對其進行操作,同上條這樣對對象集合屬性的直接修改,容易産生 bug
- Tips 19 使用清晰而協調的命名方式
- 方法名的風格要保證與自己的代碼或是需要內建的架構一緻,也就是上下文需要一緻,這點最重要放第一
- 起名遵循 Objective-C 的命名規範,這樣的接口名字一定程度上提示了接口的作用
- 方法名言簡意赅,從左到右讀起來最好像一個日常用于中的句子
- 方法名裡不要使用縮略後的類型名稱
- Objective-C 的方法名相較其他語言要長一些,但是可以更好地表達方法的作用,以及各個參數的意義,比如:
Rectangle *recgangle = new Rectangle(5.0f, 10.0f); // 不如下面的命名方式 Rectangle *recgangle = [Rectangle initWithSize:(float)width :(float)height]; // 不如下面的命名方式 Rectangle *recgangle = [Rectangle initWithWidth:(float)width andHeight:(float)height];
- Tips 20 為私有方法名加字首
- 因為在 Objective-C 中沒有私有方法,所有對象都可以響應任意消息,并且可以通過 runtime 擷取對象可以相應的消息,是以我們使用特定的命名來區分私有方法
- 在使用 Category 或繼承系統中或第三方庫中的類的時候,可以防止命名沖突
- C 語言中使用
下劃線作為系統内部函數的開頭是以我們不能使用_
作為私有方法的字首(蘋果的官方庫也使用_
)_
- 原書作者建議使用
來作為私有方法的字首,個人建議使用開發中項目使用的字首小寫來作為類字首,比如上文的p_
中添加私有方法可以使用EOCPerson
,這樣的字首在第三方類庫中出現重複的機率比較小eco_privateMethodName:
- Tips 21 了解 Objective-C 錯誤模型
- ARC 在預設情況下并不是異常安全的,也就是抛出異常的時候,在作用域末尾應該釋放的對象将不會被釋放
-
來告訴編譯器需要生成異常安全的代碼,但是這樣會引入一些額外代碼,并且在不抛出異常時也會執行這部分代碼-fobjc-arc-exceptions
- 就算不使用 ARC 使用異常也很容易寫出記憶體洩漏的代碼,因為需要在抛出異常前清理所有申請的資源,是以現在我們隻在非常罕見(嚴重錯誤,比如:抽象類中的方法沒有實作)的情況下抛出異常,抛出之後不需要考慮回複的問題,并且退出應用,這樣就不用編寫複雜的異常安全代碼
- 對于不嚴重的錯誤,我們通過傳回 nil/0 或是使用
來處理,NSError
中包含了錯誤處理所需的各種資訊,我們自己的錯誤需要規劃和設定好對應的 Error Domain,Error CodeNSError
- 一般通過 delegate 來傳遞錯誤
或是輸入參數傳回錯誤- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
- (BOOL)doSomething:(NSError **)error
- Tips 22 了解
協定NSCopying
- 實作
接口可以讓類實作拷貝(NSCopying
)方法,copy
中的- (id)copyWithZone:(NSZone *)zone
是以前開發時使用的記憶體區參數,目前已經不使用了,可以不用考慮他zone
-
協定支援可變拷貝(NSMutableCopying
)方法mutableCopy
- 對象拷貝時需要決定是深拷貝還是淺拷貝,一般情況下用淺拷貝
- 絕大多數情況下
實作的都是淺拷貝,是以如果使用深拷貝,建議建立一個單獨的方法來完成NSCopying
- 實作
Chapter 4 協定(Protocol)和分類(Category)
- Tips 23 使用委托(delegate)和資料源(data source)協定進行對象間通信
- 委托模式(delegate pattern):對象把應對某個行為的責任委托給了另一個類
- 類似我們經常使用的
,UITableView
UITableViewDelegate
分别定義了如何處理事件的接口和如何提供資料的接口,實作這兩個接口為UITableViewDataSource
提供互動邏輯和顯示資料,UITableView
本身隻負責顯示擷取到的資料UITableView
- 委托模式同樣适用于異步事件,比如網絡請求完成後,回調委托對象将結果傳遞回去,實作事件的異步處理
- 使用委托對象的對象中的委托對象屬性需要設定為 weak,防止循環引用
- 使用委托中的方法時,使用
先查詢委托對象是否實作了該方法,特别是在協定中使用respondsToSelector:
關鍵字标注的可選方法@option
- 委托中的方法名要清晰明确,需要說明事件的來源,目前的事件,以及為什麼委托對象需要擷取這個事件,所有委托方法都需要将發起委托的對象發送到委托對象(作為第一個參數),讓委托對象判斷事件來源
- 針對需要進行多次調用的委托對象(例如網絡加載時下載下傳進度),可以通過結構體等方法,在設定委托對象的時候,一次檢查需要響應的方法并記錄,之後在使用的時候,直接通過記錄結果來判斷是否實作了某個方法,不用每次都使用
方法來查詢是否實作,例:respondsToSelector:
@interface EOCNetworkFetcher() { struct { unsigned int didReceiveData : 1; unsigned int didFailWithError : 1; unsigned int didUpdateProgressTo : 1; } _delegateFlags; } @end @implementation EOCNetworkFetcher - (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate { _delegate = delegate; _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]; _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)]; _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)]; } @end // 在需要調用 delegate 方法的時候 if (_delegateFlags.didUpdateProgressTo) { [_delegate networkFetcher:self didUpdateProgressTo:currentProgress]; }
- Tips 24 将類的實作代碼分散到便于管理的多個 Category 中
- 在開發的過程中,類的代碼隻會越來越大,那麼我們可以通過分類機制将類的代碼打散,根據業務分散到不同的分類中
- 應該把私有方法放到叫(Private)的分類中,隐藏實作細節
- Tips 25 總是為第三方分類的分類名稱加字首
- 如果分類中出現同名方法,容易出現奇怪的 bug,是以在為其他類添加分類的時候,分類名稱和分類中的方法需要添加你自己使用的字首
- Tips 26 勿在分類中聲明屬性
- 分類中可以定義方法(包括 getter 和 setter),但是不要定義屬性,因為在分類中定義的屬性不會生成執行個體變量
- 雖然有
魔法可以用,但是這容易導緻記憶體管理問題,因為無法使用屬性記錄記憶體管理語義,但是建議一般情況下不使用objc_setAssociatedObject
- 分類的主要作用是擴張類的功能,而不是封裝資料
- Tips 27 使用 Class-Continuation 分類,隐藏實作細節
- Class-Continuation 分類必須定義在該類的實作檔案中,并且可以聲明執行個體變量,并且建議僅以此種方式增加執行個體變量
- 頭檔案中聲明為隻讀的屬性,可以在實作檔案中的 Class-Continuation 分類中擴充為可讀寫
- 私有方法原型,和私有屬性,都可以放到 Class-Continuation 分類中
- 在 Class-Continuation 分類中可以聲明實作的接口,并且外部不會知道
- 可以通過私有屬性很好的封裝 C++/Objective-C++ 的代碼,提供 Objective-C 的接口給其他代碼使用
- Tips 28 通過協定提供匿名對象
- 使用類似
提供匿名類型對象作為 delegate,可以隐藏類名@property(nonatomic, weak) id<ProtocolName> delegate;
- 對于類型不重要,隻需要提供可向應方法的對象,可以使用匿名對象,隐藏實作細節
- 使用類似
對于 Chapter 1 的補充
第一章第四條中,多用類型常量,少用
#define
預處理指令中,建議大家使用類型常量而不是
#define
來定義常量,這裡增加一個補充内容,swift 中,我們可以使用
struct
中的靜态變量來聲明常量,這樣帶來的一個好處是使用和分類管理非常友善
Xcode 8.0 帶的 clang 4.0 後開始支援類常量,也就是定義屬性的時候,可以加入
class
來修飾屬性,這樣這個屬性是屬于類的,于是乎,我們可以這樣使用常量了
NSString *notificationName = XXXConstant.notificationNames.XXXUserDidLoginNotificationName;
看上去比類型常量長一些,不過似乎還算比較好看
定義的時候需要這樣定義:
@interface XXXConstantNotificationNames : NSObject
@property(nonatomic, readonly) NSString *XXXUserDidLoginNotificationName;
@end
@interface XXXConstant : NSObject
@property(nonatomic, class, copy) XXXConstantNotificationNames *notificationNames;
@end
并且,類常量是不會被 synthesize 的,也就是說編譯器不會自動為類常量建立相應的變量,是以在實作檔案中,我們需要這麼寫
@implementation XXXConstantNotificationNames
- (NSString *)XXXUserDidLoginNotificationName {
return @"XXXUserDidLoginNotificationName";
}
@end
@implementation XXXConstant
static XXXConstantNotificationNames *_notificationNames = nil;
+ (void)load {
_notificationNames = [[XXXConstantNotificationNames alloc] init];
}
- (XXXConstantNotificationNames *) {
reutrn _notificationNames;
}
@end
看上去比定義一個
kXXXUserDidLoginNotificationName
字元串常量,麻煩了非常多,但是相信在項目代碼量不斷增加,以及工程變得越來越複雜以後,這樣的做法對于代碼管理上是非常有幫助的