天天看點

Effective Objective-C 2.0 Tips 總結 Chapter 3 & Chapter 4

Chapter 3 接口與 API 設計

  • Tips 15 使用字首避免明明空間沖突
    • Objective-C 沒有命名空間,是以我們在起名時要設法避免命名沖突
    • 避免命名沖突的方法就是使用字首
    • 應用中的所有名稱都需要加字首(包括實作檔案中的全局變量和純 C 函數)
  • Tips 16 提供“全能(designated)初始化方法”
    • 一個會被所有初始化方法調用到的初始化方法
    • 當底層資料存儲機制變化時,隻需要修改這個方法就可以了,不需要改動其他初始化方法
    • 如果超類的全能初始化方法不适用于子類,或是與超類不同,那麼需要覆寫這個超類方法
    • 子類的全能初始化方法都應該調用超類的對應方法,逐級向上
  • Tips 17 實作 description 方法
    • 在數組字典等集合對象列印時,都會調用對象的

      description

      方法,友善調試
    • 系統預設的

      description

      方法對于自定義的對象并沒有輸出較為有用的内容,是以可以實作這個方法友善我們顯示對象
    • 在調試時會調用

      debugDescription

      方法(也就是在調試時 lldb 中輸入 po 時調用的将會是

      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

      來處理,

      NSError

      中包含了錯誤處理所需的各種資訊,我們自己的錯誤需要規劃和設定好對應的 Error Domain,Error Code
    • 一般通過 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 通過協定提供匿名對象
    • 使用類似

      @property(nonatomic, weak) id<ProtocolName> delegate;

      提供匿名類型對象作為 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

字元串常量,麻煩了非常多,但是相信在項目代碼量不斷增加,以及工程變得越來越複雜以後,這樣的做法對于代碼管理上是非常有幫助的

繼續閱讀