1. 程式人生 > >Effective Objective-C 2.0 總結與筆記(第三章)—— 介面與API設計

Effective Objective-C 2.0 總結與筆記(第三章)—— 介面與API設計

第三章:介面與API設計

​ 在開發應用程式的時候,總是不可避免的會用到他人的程式碼,或者自己的程式碼被他人所利用,所以要把程式碼寫的更清晰一點,方便其他開發者能夠迅速而方便地將其整合到他們的專案裡。

第15條:用字首避免名稱空間衝突

  • Objective-C沒有內建的名稱空間機制(namespace),所以命名的時候需要設法避免潛在的命名衝突,否則就很容易重名了。如果發生命名衝突,那麼應用程式的連結過程就會出錯。比無法連結更糟糕的是在執行期載入了含有重名類的程式庫,這時候就會有"重名符號錯誤",很可能導致整個程式崩潰。

  • 為了避免這種問題,我們可以變相實現名稱空間:為所有的名稱加上這個程式相關的字首。需要注意的是,Apple宣稱保留使用所有“兩字母字首“的權利,所以自己選用的字首應該是3個字母。

  • 如果在第三方庫的基礎上進行了開發後再次封包,則需要將所有的檔案字首修改成和第三方庫之前匹配的字首,才可以封包給其他人使用。

    example:EOCLibrary裡擴充套件了XYZLibrary的東西,那麼就要把XYZLibrary的相關字首改成EOC的字首。

第16條:提供“全能初始化方法”

  • 所有物件均要初始化,可為物件提供必要資訊以便其能完成工作的初始化方法叫做全能初始化方法。如果建立類例項的方法不止一種,那麼這個類就會有多個初始化方法,這裡需要選定一個初始化方法作為全能初始化方法,令其他初始化方法都呼叫它。
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;

在上面的程式碼裡,initWithTimeIntervalSinceNow就是全能初始化方法,其他所有的初始化方法都要呼叫它,所以只有該方法才會儲存內部資料,當內部資料改變的時候,僅需要改變全能初始化方法即可。

  • 如果子類的全能初始化方法與超類的不同,那麼應該覆寫超類的全能初始化方法。
  • 在Objecive-C程式中,只有發生嚴重錯誤的時候才應該丟擲異常,初始化的時候丟擲異常是不得已之舉,表明例項真的沒辦法初始化了。

第17條:實現description方法

  • 除錯程式的時候,經常需要列印並檢視物件資訊,一般有兩種方法:

    • 編寫程式碼把物件的全部屬性輸出到日誌中。
    • 直接列印物件,通過實現description方法,將資訊打印出來。
    //EOCPerson.h
    @interface EOCPerson : NSObject
    
    @property(nonatomic, copy) NSString *firstName;
    @property(nonatomic, copy) NSString *lastName;
    
    @end
    
    //EOCPerson.m
    - (NSString *)description {
        return [NSString stringWithFormat:@"<%@: %p,\"%@ %@\">",
                [self class], self, _firstName, _lastName];
    }
    
    //test
    EOCPerson *person = [EOCPerson new];
    person.firstName = @"Bob";
    person.lastName = @"Smith";
    NSLog(@"person = %@", person);
    /**
    person = <EOCPerson: 0x7bf240c030f0,"Bob Smith">
    **/
    
  • 如果要輸出很多互不相同的資訊,可以藉助NSDictionary

- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: %p,%@>",
            [self class],
            self,
            @{@"firstName":_firstName,
              @"lastName":_lastName}
            ];
}
  • 如果想在LLDB裡通過po指令輸出,那麼就應該實現debugDescription方法,和Description方法一樣。

第18條:儘量使用不可變物件

  • 在實際程式設計中,應該儘量把對外公佈的屬性設為只讀,而且只在確有必要的時候才將屬性對外公佈。
  • 如果使用了readonly屬性,那麼有人試著改變屬性值,編譯器就會報錯。這樣只能在物件內部進行修改。
  • 有時可能想修改封裝在物件外部的屬性,卻不想令這些資料為外人所動,這種情況下可以利用"class-continuation分類",將readonly改為readwrite。其實是等於在內部使用Extension的方式來實現。在物件外部,也可以通過KVC的方式來設定這些屬性。
//EOCPointInterest.h
@interface EOCPointInterest : NSObject

@property (nonatomic, copy, readonly) NSString *identifier;

@end

//EOCPointInterest.m
#import "EOCPointInterest.h"
@interface EOCPointInterest()

@property (nonatomic, copy, readwrite) NSString *identifier;

@end

@implementation EOCPointInterest
...
@end

//outter
[pointOfInterest setValue:@"GDGD" forKey:@"identifier"];

這樣可以直接改動identifier屬性,即使沒有於公共介面中公佈這個方法,它依然可以違規的繞過本類的API。

  • 定義一些公共API的時候,可能會涉及到collection,這些屬性應該設定成可變還是不可變是需要好好考慮清楚的。如果是可變的collection的話,不要把可變的collection作為屬性公開,而應該提供相關方法讓別人通過方法去修改collection。

第19條:使用清晰而協調的命名方式

  • 這一章暫且略過,畢竟命名還是一個比較複雜的部分,而且每個工程都有自己的一套特色。這裡只簡單說一下大原則。
  • 在Objective-C裡,命名要求儘量清晰,不使用縮略,讓程式碼讀起來像句子一樣。
  • 類和協議的命名應該加上字首,避免名稱空間衝突。

第20條:為私有方法名加字首

  • 一般來說私有方法是指在內部使用不暴露在外面的方法,這種方法可以通過加字首的方式方便除錯,同時也可以通過字首來查詢以方便統一修改私有程式碼。
  • 不要單用一個下劃線作為私有方法的字首,這是預留給蘋果公司用的。

第21條:理解Objective-C錯誤模型

  • 在Objective-C裡,自動引用計數預設情況下不是異常安全的,也就是如果丟擲異常那麼該物件就不會自動釋放了。如果要異常安全的話需要做一些額外操作。
  • 即使使用ARC,也可能導致記憶體洩露:
id someResource = [someClass new];
if (/* check for error */) {
    @throw [NSEXception exceptionWithName:@"Exception"
            reason:@"ther is a error"
            userInfo:nil];
}
[someResource doSomething];
[someResource release];

如果上述程式碼發生了異常之後,那麼資源就不可能被釋放掉了。如果要正確釋放應該在丟擲異常之前釋放掉資源。

  • 現在Objective-C採用的方法是儘量不丟擲異常,如果丟擲異常了無需考慮恢復問題,應用程式也應該退出。

  • 如果不是那麼嚴重的異常,一般可以令方法返回nil/0,或者是使用NSError,表明有錯誤發生。

  • NSError的用法更加靈活,並且這個模型可以新增描述錯誤的原因。

    • Error domain (錯誤範圍,型別為字串):錯誤發生的根源,通常用一個特有的全域性變數來定義。
    • Error code (錯誤碼,型別為整數):獨有的錯誤程式碼,用來指明在某個範圍內具體發生了何種錯誤。
    • User info (使用者資訊,型別為字典):關於錯誤的額外資訊。

    NSError的常見用法主要有兩種:

    • 通過委託協議來傳遞此錯誤,有錯誤發生時,當前物件會把錯誤資訊經由協議中的某個方法給其委託物件。例如:
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
    
    • 經由輸出引數返回給呼叫者,例如:
    - (BOOL)doSomething:(NSError **)error;
    
    //example
    NSError *error = nil;
    BOOL ret = [object doSomething:&error];
    if (error) {
        //handle the error
    }
    

    ​ 實際上使用ARC的時候,編譯器會把方法簽名中的NSError **轉換成NSError * __autoreleasing*,也就是指標所指的物件在方法執行完畢後自動釋放。

    ​ 定義錯誤碼的時候最好使用列舉的方式實現,並且最好在定義這些列舉的標頭檔案裡對每個錯誤型別詳細說明。

第22條:理解NSCopying協議

  • 如果想要自己的類支援copy方法,那就要實現NSCopying協議,該協議只有一個方法:
- (id)copyWithZone:(nullable NSZone *)zone;

NSZone是以前開發程式時,會把記憶體分成不同的區,而物件會建立在不同的區,現在每個程式只有一個預設區,儘管必須實現這個方法,但是zone引數不用擔心。copy方法是由NSObject實現,該方法只是以預設區為引數來呼叫。

example:

//EOCPerson.h
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject<NSCopying>

@property(nonatomic, copy) NSString *firstName;
@property(nonatomic, copy) NSString *lastName;

@end
    
//實現NSCopying的方法
- (id)copyWithZone:(NSZone *)zone {
    EOCPerson *copy = [[self class] allocWithZone:zone];
    copy.firstName = _firstName;
    copy.lastName = _lastName;
    return copy;
}
  • 如果要訪問類的內部例項變數(並非屬性),那麼需要用->的語法來訪問。
@implementation EOCPerson {
    NSMutableSet *_friends;
}

//如果要訪問,那麼就是
- (id)copyWithZone:(NSZone *)zone {
    /* copy something*/
    copy->_friends;
}
  • 除了copy方法以外,還有mutableCopy方法,這個是來自於NSMutableCopying的協議,如果要實現一個可變的拷貝,那麼就需要實現該協議的方法:
- (id)mutableCopyWithZone:(nullable NSZone *)zone;

對於不可變的NSArray與可變的NSMutableArray來說,下列關係成立:

[NSArray copy] => NSArray;
[NSArray mutableCopy] => NSMutableArray;

[NSMutableArray copy] => NSArray;
[NSMutableArray mutableCopy] => NSMutableArray;
  • 在編寫拷貝方法的時候,還需要注意一個問題那就是拷貝執行的是深拷貝還是淺拷貝。

    • 深拷貝:拷貝物件的時候底層資料也一併複製過去。
    • 淺拷貝:只拷貝容器本身,而不復制其中資料。Foundation框架的所有collection類在預設情況下都執行淺拷貝,因為collection裡可能存在無法拷貝的資料,或者並非需要深拷貝。
    - (id)deepCopy {
        EOCPerson *copy = [[[self class] alloc] init];
        copy.firstName = _firstName;
        copy.lastName = _lastName;
        return copy;
    }