Swift 與 Objective-C

Objective-C id 型別完全掌握(動態型別與鴨子型別總整理)

第一次學習 Objective-C 時,看到 id 這個型別,多少都會停頓一下。

閱讀 4 分鐘
Objective-C id 型別完全掌握(動態型別與鴨子型別總整理) 封面圖

第一次學習 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指標。

id 的本質,原來只是帶有一個 isa 的指標
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];  // 只要能像鴨子一樣叫,就當作鴨子處理
}

objDuck類別還是Robot類別,完全沒有關係。

只要能回應quack這個訊息,對我們的程式來說就是「鴨子」。

這和以繼承為基礎的多型性不同。

即使沒有繼承相同的父類別,彼此毫無關係的類別只要擁有相同的方法名稱,就能以相同方式處理。

正是這種彈性,成為 delegate 模式、target-action 等 Cocoa 核心設計的基礎。

只要能回應方法就算鴨子,這個想法很有趣吧
只要能回應方法就算鴨子,這個想法很有趣吧

方便卻危險的 id,什麼時候該小心?

id越自由,代價也越大。

由於編譯器幾乎不做型別檢查,傳送無法回應的訊息時仍能通過編譯,卻會在執行中讓 App 當掉。

這就是著名的unrecognized selector sent to instance崩潰。

因此實務上會這樣使用。

  • 型別確定時,明確指定具體類別(例如NSString *),不要使用id
  • 必須使用id時,在呼叫前用respondsToSelector:防護
  • 定義 delegate protocol,將「會回應這個訊息」記錄在文件中

近年導入了instancetypeNSArray<NSString *> *等泛型,因此id的濫用已大幅減少。

即使如此,在框架底層與處理 runtime 的程式碼中,id仍像心臟一樣持續跳動。

這段紅色日誌,就是誤用 id 時最常遇到的畫面
這段紅色日誌,就是誤用 id 時最常遇到的畫面

總結來說,id是象徵 Objective-C 動態特性的型別,而動態型別與鴨子型別都延伸自「依行為而非類別判斷物件」的理念。

一開始可能覺得陌生又危險,但理解這個原理後,Cocoa 框架的設計會清晰許多。

希望今天讓你困惑的id,現在不再那麼令人不知所措。祝你享受 Objective-C 的學習!