第一次學習 Objective-C 時,看到id這個型別,多少都會停頓一下。
你可能會疑惑:「這到底是什麼型別?為什麼放入任何物件都能通過編譯?」
先說結論,id是「可以指向任何物件的萬用指標」,也是支撐 Objective-C 動態型別與鴨子型別的核心。
本文將逐一整理id型別的確切定義、動態型別與鴨子型別在實際程式碼中的運作方式,以及何時該使用、何時該提高警覺。
id 型別究竟是什麼?
簡單說,id就是「指向任意 Objective-C 物件的指標」。
不必指定NSString *、NSArray *等具體類別,也能存放任何物件。
其實id的本質比想像中簡單,內部定義如下。
// objc.h 內部定義(摘要)
typedef struct objc_object {
Class isa; // 指出這個物件屬於哪個類別的指標
} *id;
也就是說,id是指向某個結構的指標,而該結構只有一個isa指標。
這個isa就像標籤一樣,在執行階段告訴系統「我是某個類別的實例」。
有趣的是,id本身已經是指標,所以寫成id obj,不會像id *obj那樣再加上星號。
這一點和NSString *str比較時很容易混淆。
動態型別是這樣運作的
動態型別(dynamic typing)是指「在執行階段,而不是編譯階段判斷物件的實際型別」。
id中物件實際屬於哪個類別,會在程式執行時查看isa指標後決定。
因此可以撰寫這樣的程式碼。
id obj = @"Hello"; // 目前是 NSString
NSLog(@"%@", [obj class]); // 輸出: __NSCFConstantString
obj = @[@1, @2, @3]; // 現在變成 NSArray
NSLog(@"%@", [obj class]); // 輸出: __NSArrayI
同一個變數obj會從字串變成陣列。
編譯器不會阻止這件事,因為實際型別的判斷被延後到執行階段。
傳送訊息時也一樣。呼叫[obj length]時,編譯階段無法知道obj是否會回應length。
執行階段會當場檢查obj的類別,找到對應方法後執行。這稱為訊息分派。
兩者比較如下。
- 靜態型別:編譯器事先確定並檢查型別
- 動態型別:執行中查看實際物件後決定
在 Objective-C 中,
id與動態型別體現了「先放進去,真正是什麼等執行時再問」的理念。
鴨子型別真正的意義是什麼?
這裡是許多人容易誤解的地方:把鴨子型別籠統理解成「不做型別檢查」。
鴨子型別原本的說法是:
「走路像鴨子、叫聲像鴨子,那就是鴨子。」
重點不在物件屬於哪個類別,而在於「它能否回應那個訊息」。
在 Objective-C 中,可以使用respondsToSelector:直接確認這件事。
// 不論類別為何,只要能回應這個方法就呼叫
if ([obj respondsToSelector:@selector(quack)]) {
[obj quack]; // 只要能像鴨子一樣叫,就當作鴨子處理
}
obj是Duck類別還是Robot類別,完全沒有關係。
只要能回應quack這個訊息,對我們的程式來說就是「鴨子」。
這和以繼承為基礎的多型性不同。
即使沒有繼承相同的父類別,彼此毫無關係的類別只要擁有相同的方法名稱,就能以相同方式處理。
正是這種彈性,成為 delegate 模式、target-action 等 Cocoa 核心設計的基礎。
方便卻危險的 id,什麼時候該小心?
id越自由,代價也越大。
由於編譯器幾乎不做型別檢查,傳送無法回應的訊息時仍能通過編譯,卻會在執行中讓 App 當掉。
這就是著名的unrecognized selector sent to instance崩潰。
因此實務上會這樣使用。
- 型別確定時,明確指定具體類別(例如
NSString *),不要使用id - 必須使用
id時,在呼叫前用respondsToSelector:防護 - 定義 delegate protocol,將「會回應這個訊息」記錄在文件中
近年導入了instancetype與NSArray<NSString *> *等泛型,因此id的濫用已大幅減少。
即使如此,在框架底層與處理 runtime 的程式碼中,id仍像心臟一樣持續跳動。
總結來說,id是象徵 Objective-C 動態特性的型別,而動態型別與鴨子型別都延伸自「依行為而非類別判斷物件」的理念。
一開始可能覺得陌生又危險,但理解這個原理後,Cocoa 框架的設計會清晰許多。
希望今天讓你困惑的id,現在不再那麼令人不知所措。祝你享受 Objective-C 的學習!

