Effective Objective-C 2.0 讀書筆記三

第三章 接口與API設(shè)計

一份好的代碼,不光自己能夠看懂,也應(yīng)該讓別人很容易理解,并且我們要確保代碼添加的別的工程中的時候,不會影響其他代碼,這時候,好的接口和API設(shè)計就非常有用了。

15. 用前綴避免命名空間沖突

簡單的說就是不能在同一個工程中出現(xiàn)相同的類名,解決的辦法就是加前綴。Apple宣稱保留所有“兩字母前綴”,所以大部分代碼的前綴都是三個大寫字母,這些字母可以是任意的,可以根據(jù)公司名、項目名等。應(yīng)用程序中所有的名稱都應(yīng)該加前綴,包括“分類”及“分類”中的方法等。
另外需要注意的一點,我們在寫一份第三方應(yīng)用的時候,如果在我們的文件中引入了其他第三方代碼,一定要給第三方代碼加前綴,雖然這是一個很枯燥的過程,因為引用我們代碼的人可能也引入了那個第三方代碼,這個時候就會起沖突,所以這一點一定要注意,例如Reachability文件的引用。

16. 提供“全能初始化方法”

對象的產(chǎn)生需要初始化,有時候一個類可能存在多個初始化方法,這樣做可以讓我們根據(jù)自己的需求創(chuàng)建出我們想要點的實例對象,不過這樣做,我么要在這些初始化方法中選擇一個“全能初始化方法”,令其他的初始化方法都來調(diào)用發(fā)。全能初始化方法的定義是這樣的,我們把這種可為對象提供必要信息以便其能完成工作的初始化方法,叫做全能初始化方法。
其實運(yùn)用全能初始化方法的最直觀的好處就是,當(dāng)?shù)讓訑?shù)據(jù)存儲機(jī)制變動的時候,只要修改全能初始化方法就可以了。
下面用例子說明如何創(chuàng)建一個類的全能初始化方法,以及如何創(chuàng)建這個類的子類的全能初始化方法:
首先定義一個矩形類:
.h文件

@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;
- (id)initWithWidth:(float)width height:(float)height;
@end

.m文件

@implementation EOCRectangle
- (id)initWithWidth:(float)width height:(float)height{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}
@end

只這么寫還是不行的,因為有時候可能會用[[EOCRectangle alloc] init]的方法創(chuàng)建實例(這個方法是從NSObject繼承過來的),這時候我們應(yīng)該覆寫init初始化方法,如下:

// 第一種方案
- (instancetype)init{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"必須用initWithWidth:Height: 初始化方法" userInfo:nil];
}
// 第二種方案
- (instancetype)init{
    return [self initWithWidth:5.0 height:10.0];
}

個人推薦第一種方案,因為第二種方案在底層數(shù)據(jù)變動的時候還是要修改,并且我們可能不想要一個默認(rèn)的值。
下面定義一個正方形類EOCSquare,繼承于上面的矩形類:
.h文件

@interface EOCSquare : EOCRectangle
- (id)initWithDimension:(float)dimension;
@end

.m文件

@implementation EOCSquare
- (id)initWithDimension:(float)dimension{
    return [super initWithWidth:dimension height:dimension];
}
@end

這樣寫很合理,在子類的全能初始化方法中調(diào)用了父類全能初始化方法,我們在繼承體系中,一定剛要確保這樣的鏈?zhǔn)浇Y(jié)構(gòu)延續(xù)下去,這個時候我們還可能用initWithWidth:Height:init方法初始化EOCSquare類,這時候我們還要覆寫這兩個方法,如下:
覆寫initWithWidth:Height:方法

// 第一種方案
- (id)initWithWidth:(float)width height:(float)height{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"必須用initWithDimension:初始化方法" userInfo:nil];
}
// 第二種方案
- (id)initWithWidth:(float)width height:(float)height{
    float dimension = MAX(width, height);
    return [self initWithDimension:dimension];
}

個人還是推薦第一種方案
覆寫init方法

// 第一種方案
- (instancetype)init
{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"必須用initWithDimension:初始化方法" userInfo:nil];
}
// 第二種方案
- (instancetype)init
{
    return [self initWithDimension:0.5];
}

個人還是推薦第一種方案
有時候,一個類的實例可能來自完全不同的兩種初始化方式,例如我們上面定義的矩形類,如果遵循了NSCoding協(xié)議,那么我們就要另外添加一種全能初始化方法

- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super init])) {
        _width = [aDecoder decodeFloatForKey:@"width"];
        _height = [aDecoder decodeFloatForKey:@"height"];
    }
    return self;
}

一個類的父類遵循了NSCoding協(xié)議,按照上面的例子,此時若EOCSquare類也遵循NSCoding協(xié)議的話,應(yīng)該也增加全能化初始化方法,并且可以調(diào)用父類的全能初始化方法,如下:

- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super initWithCoder:aDecoder])) {
        // 處理子類自己的需求
    }
    return self;
}

我們始終要遵循子類的全能初始化方法調(diào)用父類全能初始化方法這一規(guī)定,例如本例中,如果不調(diào)用EOCRectangle的initWithCoder:方法的話,就無法將_width_height兩個實例變量解碼。

17. 實現(xiàn)description方法

description方法定義在NSObject協(xié)議里,NSObject也實現(xiàn)了他,所以如果不在類里覆寫description方法,打印信息就會調(diào)用NSObject類實現(xiàn)的默認(rèn)方法(NSProxy基類也遵循NSObject協(xié)議)。description方法主要的用處就是在打印信息的時候調(diào)用這個方法獲取信息,如果不在自己的類里覆寫description方法,打印出來的信息只有類名和內(nèi)存地址,很顯然這不能滿足我們的需求,這時候就要覆寫description方法,增加對象的描述信息。
下面提幾個點:
1.在新實現(xiàn)的description方法中,也應(yīng)該像默認(rèn)實現(xiàn)的那樣,打印出類名和內(nèi)存地址。
2.在自定義信息內(nèi)容的時候可以遵循字典那樣的格式,這樣方便以后增加和刪除打印項,并且字典形式看起來更簡潔。
不過怎么定義打印信息全看我們自己,沒有固定的格式,只要自己用著方便就好。
NSObject協(xié)議中還有一個debugDescription方法,這個方法是開發(fā)者在調(diào)試器中以控制臺命令打印對象調(diào)用的(就是我們在控制臺中輸入po時調(diào)用),在NSObject類的默認(rèn)實現(xiàn)中debugDescription方法直接調(diào)用description方法,和description方法的覆寫一樣,具體怎么設(shè)置打印信息沒有具體標(biāo)準(zhǔn),只要自己用著方便就行。

18. 盡量使用不可變對象

設(shè)計類的時候,應(yīng)該盡量用屬性來封裝數(shù)據(jù),而在使用屬性時,則應(yīng)該盡量將屬性聲明為只讀(readonly),為什么要這樣呢?通常一個類中數(shù)據(jù)都是由網(wǎng)絡(luò)獲取的,即使我們修改這些屬性,也不會發(fā)送回服務(wù)器,所以沒有必要,另外,有的時候我們不知道屬性的內(nèi)部結(jié)構(gòu),比如集合中是否包含可變對象等,如果包含,這些可變對象是否可以更改,這些都是未知的,所以我們在設(shè)計屬性的時候盡量都聲明為只讀,這樣可以避免很多麻煩,當(dāng)然這不是固定的,真正的開發(fā)中還是看實際的需要,只是在多數(shù)情況下建議這樣做。
為了把屬性對外設(shè)置成只讀,通常將readonly的屬性在對象內(nèi)部聲明為readwrite,通常都是在分類中從新聲明一下,不過這么做需要注意一點,當(dāng)屬性是nonatomic的時候,可能產(chǎn)生“競爭條件”(內(nèi)部寫入屬性時,外部也許正在讀取屬性),若想避免這個問題可能在必要時通過“派發(fā)隊列”手段,將所有數(shù)據(jù)存儲操作設(shè)為同步操作。
需要提一點的是,即使屬性聲明為readonly,值也是可以通過外部修改的,可以通過KVC直接進(jìn)行鍵值編碼,更暴力一點可以直接用類型信息查詢功能查出屬性所多對應(yīng)的實例變量在內(nèi)存布局中的偏移量,以此來人為設(shè)置這個實例變量的值,不過這么做都是不推薦的。
另外還需要注意一點,當(dāng)我們的屬性是集合類型的時候,我們應(yīng)該將屬性設(shè)置成可變還是不可變,通過下面例子說明:
.h文件

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, copy, readonly) NSSet *friends;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
- (void)addFiriend:(EOCPerson *)person;
- (void)removeFiriend:(EOCPerson *)person;
@end

.m文件

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson{
    NSMutableSet *_internalFriends;
}
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName{
    if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
    return self;
}
- (void)addfriend:(EOCPerson *)person{
    [_internalFriends addObject:person];
    
}
- (void)removefriend:(EOCPerson *)person{
    [_internalFriends removeObject:person];
    
}
- (NSSet *)friends{
    return [_internalFriends copy];
}
@end

注意這個例子中的friends屬性,有人可能會問,為什么不用NSMutableSet來實現(xiàn)friensds屬性呢?這樣做還可以省去addFriend:和removeFriend:兩個方法,我們要知道一點,如果換成NSMutableSet,這種過分解耦的數(shù)據(jù)很容易出現(xiàn)bug,如果換成NSMutableSet省去添加朋友和刪除朋友這兩個方法,那么相當(dāng)于從底層直接修改了內(nèi)部存放朋友對象的set,在EOCPerson對象不知情時,很容易使對象內(nèi)個數(shù)據(jù)之間不一致。

19. 使用清晰而協(xié)調(diào)的命名方式

首次接觸OC的人都認(rèn)為OC的命名太長了,可能有的人喜歡有的人不喜歡,不過一種形式設(shè)計出來并且大家都在遵守就必然有其存在的理由。
和OC接觸多了我們會發(fā)現(xiàn),大家的代碼基本都遵循這樣的一套規(guī)范,方法與變量名使用“駝峰命名法”(以小寫字母開頭,其后每個單詞首字母大寫),類名也用駝峰命名法,不過首字母大寫,而且通常有前綴字母。
對于方法的命名來說,最好能答到的目標(biāo)是開發(fā)者能根據(jù)方法名知道這個方法有什么作用,并且了解其中的各個參數(shù)所表達(dá)的具體意思,新手可能不習(xí)慣寫這種長的方法名,但是慢慢的就會喜歡這種命名方式,不過有一點就是寫習(xí)慣OC代碼的人,對其他的語言可能會很不習(xí)慣,因為他們對方法名產(chǎn)生了依賴性。
類與協(xié)議名通常要加前綴,以避免命名沖突,這在前面已經(jīng)說過,另外我們可以模仿UIKit類庫的整體命名體系,模仿源代碼總是不會錯的。

20. 為私有方法名加前綴

可能有人會說,私有方法為什么還要加前綴,私有方法又不是暴露在外面,只要自己用著方便就行,其實為私有方法加前綴也是有好處的。為私有方法加固定的前綴可以將私有方法和其他的區(qū)分開,方便查找修改,另外多提一點,私有方法盡量寫在一起。另外注意一點,不要模仿蘋果用單一下劃線作為私有方法的前綴,并且這樣做也是蘋果不推薦的,因為有時候我們可能繼承蘋果原有的類寫了一個子類,如果這樣命名的話很可能無意間覆寫了父類的方法??傊覀冏詈帽WC以下兩點為私有方法命名,第一保證自己的私有方法名是獨(dú)一無二的,第二盡量使自己的私有方法方便查找。

21. 理解Objective_C錯誤模型

OC有自己的異常信息處理方式,通常有三種方法。
1.拋出異常
主要用的是exceptionWithName: reason: userInfo:方法,將錯誤信息標(biāo)出,然后拋出異常。這種處理只應(yīng)該應(yīng)用于極其嚴(yán)重的錯誤,比如前面舉例的利用了不該用的初始化方法,這種拋出異常的方法還是不建議用的,因為這樣寫的代碼很有可能因為拋出異常而變的不安全,例如下面的例子:

id someResource = /*···*/;
if (/*check for error*/) {
    @throw [NSException exceptionWithName:ExceptionName reason:@"There was an error" userInfo:nil];
}
[someResource doSomething];
[someResource release];

從例子中可以看出當(dāng)拋出異常之后后面的釋放語句沒有執(zhí)行,這樣寫的代碼是不安全的,在ARC下可以通過修改編譯器標(biāo)志避免這種情況(打開編譯器標(biāo)志叫做-fobjc-arc-exceptions),但是這樣會讓程序運(yùn)行不必要的代碼。在非ARC下我們可以手動把釋放代碼添加都前面,但是當(dāng)代碼結(jié)構(gòu)復(fù)查的時候顯然有很大的弊端,所以拋出異常這種做法只應(yīng)該在很嚴(yán)重的情況下應(yīng)用,并且異常拋出后無須考慮修復(fù),程序直接退出。
2.令返回值為nil/0
這是更不推薦的一種做法,范式就是通過條件判斷語句,在不能達(dá)到我們要求的情況的時候返回nil/0,這種方法是極力不推薦的,因為這種代碼有時候會讓給我們造成一些不必要的困擾。
3.使用NSError
這種方法是推薦的,并且NSError用法靈活,經(jīng)由這種方法,可以把導(dǎo)致錯誤的原因回報給調(diào)用者,讓調(diào)用者按照錯誤信息查找原因。
Error對象內(nèi)部通常會封裝三種信息:

  • Error domain(錯誤范圍,類型為字符串)
    產(chǎn)生錯誤的根源,通常用特有的全局變量來定義,比如NSURLErrorDomain。
  • Error code(錯誤碼,類型為整數(shù))
    獨(dú)有的錯誤代碼,用于指明某個特定范圍可能發(fā)生的一系列錯誤,這些錯誤通常采用枚舉定義,最長見得就是我們通??吹降腍TTP請求出錯時的狀態(tài)碼。
  • User info(用戶信息,類型為字典)
    有關(guān)錯誤的一些附加信息,包含錯誤的描述,或許還含有導(dǎo)致該錯誤發(fā)生的另外一個錯誤,經(jīng)由這些信息,可以將相關(guān)錯誤串成一條“錯誤鏈”。

通常在寫代碼的時候,最常用的方法是通過委托協(xié)議來傳遞NSError對象,這樣可以把錯誤模型傳遞給其他委托對象,這樣委托對象可以根據(jù)需要判斷是不是需要處理這個錯誤信息,相信這種方式大家都比較熟悉,這里不再舉例。
另一種方法是將NSError對象經(jīng)由方法的“輸出參數(shù)”返回給調(diào)用者,范式通常如下:

-(BOOL)doSomething:(NSError**)error

參數(shù)是個指針,該指針本身指向另外一個指針,那個指針指向NSError對象。可以通過如下的例子把NSError對象傳遞到輸出參數(shù)中:

-(BOOL)doSomething:(NSError **)error{
    // Do something that may cause an error
    if (/*There was an error*/) {
        if (error) {
            // Pass the 'error' through the out-parameter
            *error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
        }
        return NO;
    } else {
        return YES;
    }
}

代碼中通過*error語法為參數(shù)error“解引用”,也就是說error所指的那個指針現(xiàn)在要指向一個新的NSError對象了。里面用了一個判斷語句,這樣做的目的是如果我們對一個空指針“解引用”會造成程序崩潰,所以要保護(hù)一下,因為有的時候調(diào)用者可能不關(guān)系具體錯誤,會給error參數(shù)傳nil。
NSError對象的內(nèi)部錯誤信息可以如下定義:
.h文件

extern NSString *const ErrorDomain;
typedef NS_ENUM(NSUInteger, Error){
    ErrorUnknow                 = -1,
    ErrorInternalInconsistency  = 100,
    ErrorGeneralFault           = 105,
    ErrorBadInput               = 500,
};

.m 文件

NSString *const ErrorDomain = @"ErrorDomain";

枚舉中可以標(biāo)注出相應(yīng)的錯誤意思,至于userInfo就看錯誤的情況自行定義了。

22. 理解NSCopying協(xié)議

注意是NSCopying不是NSCoding協(xié)議
OC中對象如果要是能被copy,就要實現(xiàn)NSCopying協(xié)議,該協(xié)議只有一個方法:

- (id)copyWithZone:(NSZone *)zone;

首先不用糾結(jié)zone這個參數(shù),以前開發(fā)程序時,會根據(jù)NSZone把內(nèi)存分成不同的區(qū),而對象會創(chuàng)建在區(qū)里面,現(xiàn)在不用了,現(xiàn)在是每個程序只在一個默認(rèn)區(qū)。下面看一下NSCopying協(xié)議的具體實現(xiàn),還是以EOCPerson為例:
.h文件

@interface EOCPerson : NSObject<NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
@end

.m文件(實現(xiàn)協(xié)議中方法)

- (id)copyWithZone:(NSZone *)zone{
    EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
    return copy;
}

這是一個最基本的實現(xiàn)NSCopying的例子(注意我們直接把拷貝對象交給了“全能初始化方法”),看下面一種情況,假如EOCPerson類中有一個集合,該集合和朋友的添加和刪除有關(guān)
.h文件

@interface EOCPerson : NSObject<NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName;
- (void)addFiriend:(EOCPerson *)person;
- (void)removeFiriend:(EOCPerson *)person;
@end

.m文件

@implementation EOCPerson{
    NSMutableSet *_internalFriends;
}
- (id)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName{
    if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
    return self;
}
- (void)addFiriend:(EOCPerson *)person{
    [_internalFriends addObject:person];
    
}
- (void)removeFiriend:(EOCPerson *)person{
    [_internalFriends removeObject:person];
    
}

- (id)copyWithZone:(NSZone *)zone{
    EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
    copy->_internalFriends = [_internalFriends mutableCopy];
    return copy;
}
@end

注意這里使用的->語法,因為internalFriends不是屬性,只是一個實力變量,我們令copy對象的internalFriends實例變量指向這個復(fù)制過的集合。我們也可以聲明一個屬性,但是在這里internalFriends不對外使用,所以沒必要這么做。這里提一下為什么要拷貝internalFriends實例變量,而不是讓兩個對象共享一個集合,這樣做顯然是不行的,如果公用一個,那么改變其中一個對象,另外一個對象就隨之改變,這是我們不想要的。另外,如果本例中的集合是不可變集合,那么就不用復(fù)制。上面的兩個例子都是用的“全能初始化方法”,這么做不是必須的,有時候可能不適用,比如全能初始化方法中涉及到復(fù)雜的數(shù)據(jù)結(jié)構(gòu),而拷貝后的對象內(nèi)部數(shù)據(jù)可能沒必要這么復(fù)雜。
上面的例子中有一個方法[_internalFriends mutableCopy],通過這個方法引出一個叫NSMutableCopying的協(xié)議,這個協(xié)議與NSCopying類似,也只有一個方法:

-(id)mutableCopyWithZone:(NSZone *)zone;

在解釋NSMutableCopying之前我們要知道,一個類的可變和不可變版本要遵循下面的規(guī)則,以NSArray和NSMutableArray為例:

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

通過上面的規(guī)則,在結(jié)合實際情況,我們就可以實現(xiàn)NSMutableCopying協(xié)議,這里不再多舉例。
還有一個深拷貝和淺拷貝的問題,有時候我們要考慮是不是要給一個類添加深拷貝方法deepCopy,特別是容器類,例如NSSet類中就有一個方法:

- (instancetype)initWithSet:(NSSet<ObjectType> *)set copyItems:(BOOL)flag;

當(dāng)參數(shù)flag為YES的時候,該方法會向集合中的每個元素發(fā)送copy消息,用拷貝好的元素創(chuàng)建新集合,并返回給調(diào)用者,這個時候我們就要考慮編寫一個deepCopy方法,以EOCPerson類為例,添加深拷貝方法:

- (id)deepCopy{
    EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
    copy->_internalFriends = [[NSMutableSet alloc] initWithSet:_internalFriends copyItems:YES];
    return copy;
}

深拷貝沒有具體的協(xié)議,在寫的時候我們要依照具體的類來決定,另外需要注意一點,在執(zhí)行NSCopying協(xié)議的類中,大部分默認(rèn)情況下都是執(zhí)行的淺拷貝,深拷貝只在特殊需要時才會提出來。
最后科普一下,深拷貝和淺拷貝區(qū)別就是,深拷貝會將對象的底層數(shù)據(jù)也一起拷貝,淺拷貝只拷貝容器對象自身,對其內(nèi)部數(shù)據(jù)不進(jìn)行拷貝,F(xiàn)oundation框架中默認(rèn)都是淺拷貝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容