iOS中的NSObject*、id和instancetype
用 id
修飾和 NSObject *
修飾有何不同?
要詳細瞭解兩者的不同,需要先說一說Objective-C中的動態型別和靜態型別。
-
動態型別
動態型別指的是物件指標型別的動態性,具體是指使用
id
修飾後將物件的型別確定推遲到執行時,由賦給它的物件型別決定物件指標的型別。也就是說id
修飾的物件為動態型別物件,其他在編譯器指明型別的為靜態型別物件,通常如果不需要涉及到多型的話還是要儘量使用靜態型別(原因:錯誤可以在編譯器提前查出,可讀性好)。
// 動態型別 id obj = [[TestObject alloc] init];
-
靜態型別
一個指標變數指向特定類的物件時,使用的是靜態型別,在編譯的時候就知道這個指標變數所屬的類。使用靜態型別時,編譯器在編譯期間,會做許多的型別檢查:因為編譯器需要知道哪個物件該如何使用。
// 靜態型別 TestObject obj = [[TestObject alloc] init];
蘋果官方objc.h檔案裡有關於 id
的定義:
/// A pointer to an instance of a class. typedef struct objc_object *id;
-
id
可以用於指向所有的Objective-C物件,是一種萬能指標,類似於C語言中的void *
。 -
與
id
類似NSObject *
可以指向所有繼承自NSObject
的物件,因為在Objective-C中NSObject
是基類,絕大多數的類繼承自NSObject
,因此根據面向物件程式設計的多型特性,NSObject *
可以指向Objective-C中絕大多數的類的例項物件。
下面是我們用來測試的 TestObject
類的原始碼:
// TestObject.h #import <Foundation/Foundation.h> @interface TestObject : NSObject - (void)run; @end
// TestObject.m #import "TestObject.h" @implementation TestObject - (void)run { NSLog(@"%s",__func__); } @end
接下來我們在 ViewController
中執行以下測試 id
修飾時的程式碼:
- (void)viewDidLoad { [super viewDidLoad]; id p1 = [[TestObject alloc] init]; [p1 run]; }
執行情況:

編譯通過,並且 p1
成功呼叫 run
方法。
接下來我們在 ViewController
中執行以下測試 NSObject *
修飾時的程式碼:

編譯無法通過,報錯: NSObject
中沒有宣告 run
方法。
- 使用
id
修飾的物件是動態型別,編譯器在編譯期不會去判斷其真實型別,因此id
指向的物件不管向其傳送任何訊息,編譯器在編譯期都不會有任何報錯 - 使用
NSObject *
修飾的物件是靜態型別,在編譯期就已經明確該物件是NSObject
物件,因此當我們對該物件傳送NSObject
沒有宣告的方法時,編譯器就會果斷報錯
id
與 instancetype
的區別與聯絡
id
與 instancetype
的區別要從關聯返回型別和非關聯返回型別說起:
- 關聯返回型別
根據 Cocoa
的命名規則,滿足下述規則的方法:
1. 類方法中,以 alloc
或 new
開頭
2. 例項方法中,以 autorelease
, init
, retain
或 self
開頭
當方法返回值為 id
型別時,編譯器不會返回一個型別不明的物件,會返回一個方法所在類型別的物件,這些方法就被稱為是關聯返回型別的方法。換句話說,這些方法的返回結果以方法所在的類為型別。
- 非關聯返回型別
與關聯返回型別相反
1. 類方法中,不以 alloc
或 new
開頭
2. 例項方法中,不以 autorelease
, init
, retain
或 self
開頭
當方法返回值為 id
型別時,編譯器會返回一個型別不明的物件,即 id
型別的物件。
此處劃重點!!!此處劃重點!!!此處劃重點!!!
許多網上的資料並沒有拿出證據來證明上述結論的正確性。
一、比如當我們自己的類方法以 alloc
等開頭後,怎麼證明它是關聯返回型別,也就是說怎麼證明返回值是方法所在類的型別
二、我們的類方法不以 alloc
等開頭,怎麼證明它是非關聯返回型別,也就是說怎麼證明返回值不是方法所在類的型別
以下是我給出的證明:
// TestObject.h #import <Foundation/Foundation.h> @interface TestObject : NSObject + (id)newTestObject; + (id)allocTestObject; + (id)fuckTestObject; - (void)smallMethod; @property (nonatomic,copy) NSString *name; @end
// TestObject.m #import "TestObject.h" @implementation TestObject + (id)newTestObject { return [[TestObject alloc] init]; } + (id)allocTestObject { return [[TestObject alloc] init]; } + (id)fuckTestObject { return [[TestObject alloc] init]; } - (void)smallMethod { NSLog(@"%s",__func__); } @end
我們可以看到,我定義了三個類方法,返回值都是 id
型別,按照結論:
-
+ (id)newTestObject
以new
開頭的類方法,按照結論應該為關聯返回型別,即返回值是TestObject
型別 -
+ (id)allocTestObject
以alloc
開頭的類方法,按照結論應該為關聯返回型別,即返回值是TestObject
型別 -
+ (id)fuckTestObject
不以alloc
等開頭的類方法,按照結論應該為非關聯返回型別,即返回值是id
型別
我證明結論的思路是,既然返回值都是 id
型別,那麼如果我們利用呼叫例項方法來證明的話,在編譯期呼叫類所屬的例項方法是不會報錯的,因為 id
是動態型別,在編譯期不會判斷其真實型別,程式啟動執行後,由於返回的的確是 TestObject
物件,所以我們不論是在編譯期還是執行時都看不出其返回值到底是不是如結論所說的 TestObject
物件。因此, 我們可以通過呼叫例項物件的屬性來證明, id
型別可以接收任何訊息,但是卻無法使用點語法獲取屬性。若返回值是 TestObject
物件,那麼使用點語法便不會報錯,如果不是 TestObject
物件,使用點語法獲取屬性就一定會報錯。
程式碼如下:
- (void)viewDidLoad { [super viewDidLoad]; [[TestObject allocTestObject] smallMethod]; [[TestObject newTestObject] smallMethod]; [[TestObject fuckTestObject] smallMethod]; NSString *temp; temp = [TestObject allocTestObject].name; temp = [TestObject newTestObject].name; temp = [TestObject fuckTestObject].name; }

如圖所示,所有返回值呼叫方法都可以通過編譯,因為返回的都是 id
型別。但是使用點語法時 fuckTestObject
的返回值報錯,因為此方法的返回值不是關聯返回型別,返回的不是 TestObject
型別,而是 id
型別。因此證明結論是正確的。
說完了關聯返回型別和非關聯返回型別,該說說 instancetype
了
instancetype
其實就是為了擴大關聯返回型別的範圍,讓不是以上述關鍵字開頭的方法的返回值也是關聯返回型別。
我們將程式碼稍作調整:
// TestObject.h + (instancetype)fuckTestObject;
// TestObject.m + (instancetype)fuckTestObject { return [[TestObject alloc] init]; }

fuckTestObject
方法的返回值型別從
id
換成
instancetype
後,編譯就通過了,也就是說返回值是關聯返回型別了。
總結一下區別與聯絡:
-
id
和instancetype
都可以做方法的返回值。 -
id
型別的返回值在編譯期不能判斷物件的真實型別,即非關聯返回型別,instancetype
型別的返回值在編譯期可以判斷物件的真實型別,即關聯返回型別。 -
id
可以用來定義變數, 可以作為返回值, 可以作為形參,instancetype
只能用於作為返回值。