iOS 編寫高質量Objective-C程式碼(三)

QiShare發表於2018-08-20

級別: ★★☆☆☆
標籤:「iOS」「OC」「Objective-C」
作者: MrLiuQ
審校: QiShare團隊

文章目錄如下:
iOS 編寫高質量Objective-C程式碼(一)
iOS 編寫高質量Objective-C程式碼(二)
iOS 編寫高質量Objective-C程式碼(三)
iOS 編寫高質量Objective-C程式碼(四)
iOS 編寫高質量Objective-C程式碼(五)
iOS 編寫高質量Objective-C程式碼(六)
iOS 編寫高質量Objective-C程式碼(七)
iOS 編寫高質量Objective-C程式碼(八)


這一篇,將通過介紹OC的介面和API設計來提高Objective-C的程式碼質量

一、用字首避免名稱空間衝突

OC裡沒有名稱空間的概念(namespace)。於是,我們需要給類加字首,避免重名,避免發生命名衝突。當然,不僅是類名,一些全域性變數和方法也需要加上適當的字首加以區分。

所以,我們要:

  • 選擇與公司、工程相關的字首作為類名的字首。

  • 為了避免重複引用第三方庫帶來的衝突,必要時也要為他們加上字首區分。

二、提供“全能初始化方法”

  • 在類中提供一個全能初始化方法,並在文件中寫明註釋。其他的初始化方法全呼叫此全能初始化方法。
  • 好處:當類的結構發生改變或初始化邏輯發生改變時,只需要改動全能初始化方法即可。

舉個例子來說:可以看一下NSDate類中定義了一個全能初始化方法:

- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
複製程式碼

其餘的初始化方法 定義在NSDate (NSDateCreation) 分類中

- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
複製程式碼

在官方文件中,關於NSDate有如下說明 If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must:

  • Override initWithTimeIntervalSinceReferenceDate:, one of the designated initializer methods

解釋:選定一個方法作為全能初始化方法,剩下的其餘的初始化方法都呼叫這這個方法初始化,這樣做的好處是以後如果初始化的邏輯更改了只需更改全能初始化方法,或者即使子類覆寫的時候也只覆寫全能初始化方法~

三、實現 description 方法

本條寫的是通過覆寫description(或者debugDescription)方法來在NSLog列印(或者LLDB列印時)輸出更多的自定義資訊。

下面舉個例子:

- (NSString *)description {

    return [NSString stringWithFormat:@"<%@: %p, %@>",
            [self class],
            self,
            @{
              @"qi": _qi,
              @"share" : _share}
            ];
}
複製程式碼

四、儘量使用不可變物件

  1. 宣告對外屬性時,儘量使用不可變物件,同時,對外屬性宣告裡儘量加上readonly修飾~(預設是readwrite修飾)~。這樣外部只能讀取資料而不能修改資料,保證了這個類的例項所持有的資料更加安全。~尤其是不要把可變的collection作為屬性公開,而是應該提供相應的方法修改可變的collection。~
  2. 若外部想修改修改物件的值有兩種途徑:
    • 提供介面方法修改
    • 使用KVC(Key-Value Coding)技術 ~這種技術允許物件的資料或屬性可以在執行時通過其鍵名進行查詢,其中,屬性的名稱即為其值的鍵名。在靜態語言中,這樣的做法是不可能的。KVC大大的增加了設計的自由度:通過KVC,無需知道物件的型別即可訪問其屬性或資料。~

例如: 不推薦寫法:

//Animals.h
@property (nonatomic, strong) NSSet *animals;
複製程式碼

應改為:

//Animals.h
@interface Animals : NSObject

@property (nonatomic, strong, readonly) NSSet *animals;

- (void)addAnimal:(NSString *)animal;
- (void)removeAnimal:(NSString *)animal;

@end


//Animals.m
@implementation Animals {
    NSMutableSet *_mutableAnimals;
}

- (NSSet *)animals {
    return [_mutableAnimals copy];
}

- (void)addAnimal:(NSString *)animal {
    [_mutableAnimals addObject:animal];
}

- (void)removeAnimal:(NSString *)animal {
    [_mutableAnimals removeObject:animal];
}
複製程式碼

但是,小編認為這樣寫固然有好處:保證了資料的安全性,但程式碼量也會提升不少。所以推薦大家可以有選擇的使用,對一些重要的類才有使用必要。

另外,如果某屬性僅可以在物件內部修改,則可以在.h檔案中宣告為readonly。然後 在.m的類擴充套件中將屬性擴充套件為readwrite屬性。

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

師父語錄:“寫OC程式碼像是在講故事,而讀OC程式碼更像是在聽故事。”

這句話要歸功於OC清晰而協調的命名方式。

  • 首先,是駝峰式命名方法:這個和大部分程式語言都一樣。
  • 其次,也是最關鍵的方法命名。從左至右讀起來就像日常用語中的句子。

例如:我們想給初始化一個矩形,並給他的寬和高賦值。

// C++:
Rectangle *aRectangle = new Rectangle(5.0, 10.0);

// Objective-C:
Rectangle *aRectangle = [[Rectangle alloc] initWithWidth:5.0 andHeight:10.0];
複製程式碼

很顯然,OC的方法可以很直接的看出所要傳遞的引數的具體含義,而C++的傳參就並沒有這麼直觀。

六、為私有方法名加字首

這一條:給大家參考一下我們QiShare團隊制定的 iOS 程式碼規範 QiShare更喜歡通過#pragma mark -來區分 公私有等方法。 例如:

#pragma mark - Private Functions

// code...


#pragma mark - Action functions

// code...


#pragma mark - Request functions

// code...


#pragma mark - xxxDataSource

// code...


#pragma mark - xxxDelegate

// code...

複製程式碼

當然,大家也可以根據團隊自己定製規範。

七、理解 Objective-C 錯誤模型

很多語言都有異常處理機制,Objective-C也不例外。@throw
但是,
注意:OC裡拋異常會導致記憶體洩漏
注意:OC裡拋異常會導致記憶體洩漏
注意:OC裡拋異常會導致記憶體洩漏
解釋:OC裡的ARC機制(Automatic Reference Counting)在預設情況下是“無異常安全”。簡單來說,一旦丟擲異常,物件就無法正常自動釋放了。 所以,

  1. 異常只用於處理嚴重的錯誤(fatal error,致命錯誤)
  2. 對於一些不那麼嚴重的錯誤(nonfatal error,非致命錯誤),有兩種解決方案:
    • 讓物件返回nil或者0(例如:初始化的引數不合法,方法返回nil或0)
    • 使用NSError

八、理解 NSCopying協議

在iOS開發中,使用物件時經常需要拷貝它,這時我們會通過copy/mutableCopy方法完成。 如果我們想讓自己的類支援拷貝,那就必須要實現NSCopying協議,該協議只有一個方法:

- (id)copyWithZone:(nullable NSZone *)zone;
複製程式碼

當然,如果要求返回物件是可變的那就要實現NSMutableCopying協議,對應方法:

- (id)mutableCopyWithZone:(nullable NSZone *)zone;
複製程式碼

同時,在拷貝物件時,要注意是執行淺拷貝還是深拷貝

那麼引出了一個概念:什麼是深拷貝?什麼是淺拷貝?

  • 深拷貝:內容拷貝(既拷貝新的指標又拷貝出新的Object
  • 淺拷貝:指標拷貝(僅拷貝新的指標指向原來的Object

這裡有張很經典的圖解:

左邊淺拷貝,右邊深拷貝

深拷貝在拷貝物件時,會將指標所指的底層資料也拷貝一份。而淺拷貝只是建立了一個新的指標指向要拷貝的內容。一般情況下,儘量使用淺拷貝。
此外,還有一個注意點:
[NSMutableArray copy] 拷貝出 => NSArray (不可變)
[NSArray mutableCopy] 拷貝出 => NSMutableArray(可變)
這種操作可以在可變版本和不可變版本間切換。

說太多,不如給一個Demo~

  • 下面請看小編準備的NSCopying協議相關的小Demo:

QiShareMember.h:

@interface QiShareMember : NSObject <NSCopying>

@property (nonatomic, copy, readonly) NSString *name; //!< 姓名
@property (nonatomic, copy, readonly) NSString *sex; //!< 性別
@property (nonatomic, assign, readonly) NSUInteger age; //!< 年齡

//! 初始化方法
- (instancetype)initWithName:(NSString *)name andSex:(NSString *)sex andAge:(NSUInteger)age;

- (void)addFriend:(QiShareMember *)friend;
- (void)removeFriend:(QiShareMember *)friend;

@end
複製程式碼

QiShareMember.m:

@implementation QiShareMember {
    
    NSMutableSet *_friends;
}

- (instancetype)initWithName:(NSString *)name andSex:(NSString *)sex andAge:(NSUInteger)age {
    
    if (self = [super init]) {
        
        _name = [name copy];
        _sex = [sex copy];
        _age = age;
        _friends = [NSMutableSet new];
    }
    
    return self;
}

- (void)addFriend:(QiShareMember *)friend {
    
    [_friends addObject:friend];
}

- (void)removeFriend:(QiShareMember *)friend {
    
    [_friends removeObject:friend];
}

- (id)copyWithZone:(NSZone *)zone {
    
    QiShareMember *copy = [[[self class] allocWithZone:zone] initWithName:_name andSex:_sex andAge:_age];
    copy->_friends = [_friends mutableCopy]; //!< 注意friends只是一個例項變數不是一個屬性,所以不能用點語法
    
    return copy;
}

@end
複製程式碼

最後,特別緻謝《Effective Objective-C 2.0》第三章

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
程式碼快不快?跑個分就知道
iOS UIButton之改變有效點選區域(改變熱區)

相關文章