[Objective-C] id類型和instancetype類型


前些時間在源碼里看到instancetype返回類型,一臉驚異,表示接觸iOS不久沒見過這東西,但發現跟id功能差不多。故查了一些資料,了解了兩者之間的區別,故將資料簡單翻譯整理了一下,為博客充一個數 : )

轉載保留原鏈接哦原文地址

id類型

id數據類型可以存儲任何類型的對象。可以說,它是一般對象類型。
例如可以聲明一個為id類型的變量:

id graphicObject

也可聲明方法使其具有id類型的返回值:

- (id)newObject:(int)type;

id類型是Objective-C中十分重要的特性,它是多態和動態綁定的基礎。


instancetype類型

instancetype是clang3.5開始提供的一個關鍵字,表示一個未知的Objective-C對象,類似於id

按照Cocoa的慣例,Objective-C里所有使用initalloc等名稱的方法都會返回一個接受類類型的實例。這些方法被稱為“有一個關聯的返回類型”的方法,也就是說發給這些方法中的任意一個的消息都會返回一個以相同的靜態類型代替接收類類型的一個實例,例如:

@interface NSObject
+ (id)alloc;
- (id)init;
@end

@interface NSArray : NSObject
@end

和下面的通用初始化代碼:

NSArray *array = [[NSArray alloc] init];

該表達式[NSArray alloc]NSArray *類型,因為alloc擁有一個隱式的關聯的返回類型。類似的,表達式[[NSArray alloc] init]也是NSArray *類型,因為init的返回類型也是一個關聯的返回類型,同時也知道它的接收器有一個NSArray *的類型。如果allocinit都沒有一個關聯的返回類型,表達式就會返回一個id類型,如同方法簽名里聲明的一樣。

iOS 8 里很多以前返回id的方法現在都改為了instancetype,甚至initalloc。另外考慮兼容swift,還是用instancetype

可以通過聲明instancetype類型作為一個擁有關聯類型的方法的返回類型。instancetype這個上下文關鍵字只允許用在Objective-C方法的返回類型中。例如:

注意只能用在Objective-C的方法中,變量不行的哦。常見於構造方法。

@implementation oneObject
+ (instancetype)initOneObject {
    oneObject *obj = [oneObject new];
	return oneObject;
}
@end

一個關聯返回類型也可以通過一些方法推斷出來。要確定一個方法是否有一個可以被推斷出的關聯的返回類型,首先要參考駝峰命名法命名的selector中的第一個單詞(如initWithObjects中的init),其次要看其返回類型與自己的類的類型是否兼容,並且:

  • 第一個單詞是allocnew,並且方法是一個類方法(+開頭)
  • 第一個單詞是autoreleaseinitretain或者self,且方法是一個實例方法(-開頭)

如果一個擁有關聯返回類型的方法被子類方法復寫了,那么子類方法必須返回一個與子類類型兼容的類型。比如:

@interface NSString : NSObject
- (NSUnrelated *)init; // incorrect usage: NSUnrelated is not NSString or a superclass of NSString
@end

關聯的返回類型只會影響發送的消息的類型或者通過指定方法訪問屬性的類型。在其他方面,擁有關聯返回類型的方法與返回id類型的方法是一致的。


用instancetype代替id有什么好處?

instancetype可以給自定義方法一個類似alloc/init的行為,這個主要方便於構造函數

當使用id時,本質上不會有任何類型檢查。使用instancetype,編譯器和IDE知道返回的是什么類型的東西,並且更好地檢查你的代碼和自動補全代碼。舉個例子:

//Class A
@interface ClassA : NSObject

- (id)methodA;
- (instancetype)methodB;

@end

//Class B
@interface ClassB : NSObject

- (id)methodX;

@end

//Main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //這一行編譯器不會產生報錯,因為methodA方法返回的是id。但在運行時會出現異常
        [[[[ClassA alloc] init] methodA] methodX];
        //這一行不會通過編譯器的檢查,錯誤為"No visible @interface ClassA declares selector methodX",因為methodB返回instancetype,即接收器的類型。
        [[[[ClassA alloc] init] methodB] methodX];
    }
    return 0;
}

也可以說,在所有可以使用instancetype的情形中都有其好處。在詳細解釋之前,先聲明:在一個類返回一個與自己類型一致的實例時,就適合使用instancetype
實際上,Apple對於這個主題是這么解釋的:

在你的代碼中,在合適的地方用返回類型instancetype代替id類型。這通常出現在init方法和類的工廠方法。即使編譯器會自動的把以initallocnew開頭和返回類型為id的方法轉換成返回instancetype類型,除此之外它並不會轉換其他方法。Objectice-C 明確約定對所有方法都寫instancetype。來源Adopting Modern Objective-C

下面繼續,首先看幾個定義:

@interface Foo:NSObject
- (id)initWithBar:(NSInteger)bar; //initializer
+ (id)fooWithBar:(NSInteger)bar;  //class factory
@end

對於一個類工廠方法,你應該總是使用instancetype類型。編譯器不會自動將id轉換為instancetype。這個id是一個通用對象。不過你一旦將其改為instancetype,編譯器就知道這個方法返回的是一個什么類型的對象。

這並不是一個學術問題。舉例來說,以前在Mac OS下[[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]會產生如下錯誤:Multiple methods named 'writeData:' found with mismatched result, parameter type or attributes。原因就在於NSFileHandleNSURLHandle都提供一個writeData:方法。由於[NSFileHandle fileHandleWithStandardOutput]返回的是id,編譯器就不確定writeData:是哪個類調用的。
解決這個問題就需要做下列方法中的一個:

[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];

或者

NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];

當然,更好的方法就是聲明fileHandleWithStandardOutput的返回類型為instancetype。這樣,就不需要聲明類型或賦值了。

現在版本的O-C源碼里對上述例子有了修改:+ (NSFileHandle *)fileHandleWithStandardOutput;,所以不會有報錯。不過,還是有其他例子存在,比如length方法,在UILayoutSupport中返回CGFloat,在NSString里返回NSUInteger

對於初始化器,這個就更加復雜了。當你寫出如下代碼:

- (id)initWithBar:(NSInteger)bar

編譯器就會假設你輸入的是這樣的代碼

- (instancetype)initWithBar:(NSInteger)bar

這對於ARC來說很重要。見前面instancetype的定義。
這也就是為什么很多人會說使用instancetype不是必須的。當然我認為你還是應該去這么寫。下面會解釋為什么:

這有三個好處:

  1. 明確性。你的代碼的行為如同你寫的那樣,而不是其他行為。
  2. 模式化。你為此養成了一個好的代碼習慣,這有時的確很重要。
  3. 一致性。你寫的代碼前后會保持一致,增加其可讀性。

明確性
不得不承認,一個初始化方法init返回instancetype並不會帶來明顯的技術優勢。但是這只是因為編譯器會自動地將id轉換為instancetype。你若讓init 方法返回id類型,編譯器還要再解釋這個方法好像是要返回instancetype,這樣總會顯得很奇怪。
下面兩句代碼對於編譯器來說是等價的:

- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;

而對你來說,看這兩行代碼應該並不一樣。在最好的情況下而言,你會學會忽略這兩行的差別。但這並不是你應該學會忽略的,對你來說這兩句應該是不一樣的

模式化
當然init方法和其他方法沒有區別,但一旦你定義一個類工廠,那就有差別了。
下面兩行代碼並不等價:

+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

你需要第二個代碼樣式。如果你習慣去寫instancetype作為返回類型的話,你每次都會得到正確的類型。

一致性
最后,想象你把這些東西都放在一起:你想要一個init方法和一個類工廠。
如果你習慣於對init使用id類型,你的代碼看起來是這樣:

- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

但是,如果你使用instancetype,你的代碼就好看的多

- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

這樣更明確也更可讀。而且更清楚的看到他們返回同樣的東西。

結論:
除非你是故意為舊的編譯器寫代碼,否則在合適的地方你應該用instancetype
以后當你寫id之前應該三思:這個方法返回的是否是這個類的實例,如果是,就用instancetype
當然,還是會有很多需要寫id類型的情形,但你可能用instancetype會更多一些。


參考
http://clang.llvm.org/docs/LanguageExtensions.html#related-result-types
http://stackoverflow.com/questions/8972221/would-it-be-beneficial-to-begin-using-instancetype-instead-of-id
http://nshipster.com/instancetype/

Written with StackEdit.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM