揭祕instancetype

Mitsui_發表於2018-04-19

蘋果官方會建議我們用 instancetype 型別代替 id 型別作為某個類的初始化方法的返回值。以下內容摘自Adopting Modern Objective-C:

Use the instancetype keyword as the return type of methods that return an instance of the class they are called on (or a subclass of that class). These methods include alloc, init, and class factory methods.

一、初始化方法為什麼用id型別而不是 [類名] 型別作為返回值型別

instancetype 關鍵字出現之前,我們會用 id 作為類初始化方法的返回型別,在 instancetype 關鍵字出現之後,編譯器會主動將 alloc init new 開頭的方法的返回值型別替換為 instancetype, 那麼在 instancetype 出現之前,為什麼不用該類自身的型別而是用 id 型別作為初始化方法的返回值。答案是 OC 的類繼承體系。

假如,有一個類 SuperClass的初始化方法返回型別為它自己的型別,並且不會被編譯器替換為 instancetype

- (SuperClass *)init;
複製程式碼

那麼當它的子類重寫這個初始化方法的時候,只能返回它自身的例項,而無法返回子類的例項。因為重寫父類方法, 必須和被重寫的方法有相同的返回型別。所以子類永遠無法通過重寫這個父類初始化方法初始化自身。所以為了實現重寫初始化方法(用父類初始化自身)的繼承體系,必須要用一種通用型別,既能表述子類例項也能表述父類例項的型別,剛好 NSObject 是所有類的根類,由於 OC 的多型性,id 型別的變數可以指向任意型別的物件,因此,用 id 型別作為初始化方法的返回型別可以很好的解決類繼承的問題。

二、instancetype 取代 id

有的人會認為 instancetype型別和 id 型別是一種型別的不同表達方式,其實並不是。 instancetype 顧名思義是當前類的例項型別,聽起來好像和類名型別並沒有上面區別,實則,它更嚴謹的遵循 OC 的繼承體系。instancetype 型別只表述當前類的繼承線,例如 NSMutableString -> NSString ->...-> NSObject,而 id 型別相對來說更博愛一點。

在適當的地方使用 instancetype 關鍵字可以提高程式碼的型別安全。例如:

interface MyObject : NSObject
+ (instancetype)factoryMethodA;
+ (id)factoryMethodB;
@end
 
@implementation MyObject
+ (instancetype)factoryMethodA { return [[[self class] alloc] init]; }
+ (id)factoryMethodB { return [[[self class] alloc] init]; }
@end
 
void doSomething() {
    NSUInteger x, y;
 
    x = [[MyObject factoryMethodA] count]; // Return type of +factoryMethodA is taken to be "MyObject *"
    y = [[MyObject factoryMethodB] count]; // Return type of +factoryMethodB is "id"
}
複製程式碼

MyObject 宣告並實現了兩個相同類工廠方法,用來返回初始化後的 MyObject 物件,只是返回的型別一個是 instancetype,一個是 id。編譯器在程式碼 y 處不會提示任何警告和錯誤,並且在編譯期也沒有任何錯誤,但是當到了執行期就會崩潰。因為此時的 MyObject 例項可能是任意一個類的例項,只要某個類中有 -count 這個方法存在,那麼編譯器就會認為返回的例項可能是這個有 -count 方法的類,所以它不會報錯。但是,當執行期去 MyObject 類中查詢這個方法的時候,才會出現找不到這個方法並且傳送和轉發失敗的crash。關於執行時

而程式碼 y 處,該類工廠方法返回的是 instancetype 型別,該型別即為 MyObject 型別,編譯器會去它和它的父類中去尋找呼叫的方法,如果找不到那麼就會報錯,並且編譯失敗。

因此,instancetype 型別比 id 型別有更好的型別安全性,讓隱患和錯誤的暴露提前到程式碼的編寫期,避免了應用的執行時crash。

三、類工廠方法使用[self class]例項化而不是類名

例如:

@interface SuperClass : NSObject
+ (instancetype)factor;
@end

@implementation SuperClass

+ (instancetype)factor {
    return [[SuperClass alloc] init];
}

@end

複製程式碼

由於類的繼承體系,子類也可以呼叫父類方法,當子類呼叫父類的這個類工廠方法初始化自身的時候,實際上返回的例項還是父類的例項,而不是子類自身的例項,但是編譯器沒有辦法判斷這些,因為它根據類繼承體系找到了正確的方法。當向此例項傳送子類的訊息的時候,會在執行時crash,因為它會從父類例項方法列表中查詢這個子類的方法,然而父類並沒有這個方法。

NSLog(@"%@", NSStringFromClass([[SubClass factor] class]));
// log: SuperClass
複製程式碼

因此,當我們用類工廠方法初始化自身的時候,一定要用 [self class] 例項化自身,而不是類名:

+ (instancetype)factor {
    return [[[self class] alloc] init];
}
複製程式碼

這樣子類就可以通過呼叫這個父類的類工廠方法初始化自己:

NSLog(@"%@", NSStringFromClass([[SubClass factor] class]));
// log: SubClass
複製程式碼

四、單例返回型別用類名

某種程度上來說,單例的初始化方法也是一個類工廠方法,單例使用【類名】型別,而不是 instancetype 的原因是:一般情況下,不會有其他類繼承自單例類,因此,單例類在初始化的時候不用考慮對其子類的影響,因此單例類可以肆無忌憚的使用類名型別作為初始化方法的返回值型別。

相關文章