在 Objective-C 執行這段程式碼,結果相當令人意外。
NSArray *array = [NSArray arrayWithObjects:@1, @2, nil];
NSLog(@"%@", [array class]);
// 輸出: __NSArrayI
明明建立了 NSArray,類別名稱卻是__NSArrayI。空陣列可能是__NSArray0,只有一個元素時也可能是__NSSingleObjectArrayI。
這不是錯誤,而是**類別叢集(Class Cluster)**模式。
NSArray、NSString、NSNumber、NSDictionary 全都是抽象類別。
實際執行個體會依情況建立為隱藏的子類別。
這是 Effective Objective-C 2.0 第 9 項討論的內容,只要使用 Foundation 就無法避開。
先看重點(3 點)
- NSArray 等 Foundation 集合只是提供公開介面的抽象類別。
- 初始化器會選擇並回傳私有子類別的執行個體(類似工廠模式)。
- 因此,
[array class]比較很危險,子類別化也有嚴格規則。
為什麼要這樣設計?
即使只是陣列,也有多種最佳化策略。
- 0 個元素的不變陣列:不必每次建立,一個單例就夠了
- 1~2 個元素:將指標內嵌在結構中會更快
- 大型可變陣列:需要具備重新配置策略的獨立實作
Foundation 沒有把這些邏輯塞進 NSArray 的單一實作,而是準備各情境的專用子類別,再由工廠方法選擇適合者回傳。使用端只需知道 NSArray 介面,內部實作則能隨 OS 版本自由替換。
實際上,[NSArray alloc]回傳的是__NSPlaceholderArray 這個暫時物件,接續的 init 系列呼叫會將它變成真正的實作。雖然我們習慣連用 alloc 和 init,但在類別叢集中,這兩步實際上會經過不同物件。
實務上常踩的兩個地雷
地雷 1:直接比較類別
// 危險的程式碼
if ([object class] == [NSArray class]) {
// 絕對不會進到這裡,因為實際類別是 __NSArrayI 等.
}
// 正確的程式碼
if ([object isKindOfClass:[NSArray class]]) {
// 涵蓋整個叢集進行判定
}
類別叢集成員都是公開類別的子類別,因此應使用 isKindOfClass: 詢問其類別家族,而不是做相等比較。
地雷 2:草率地進行子類別化
看似只要繼承 NSArray 並覆寫一個方法,但這樣的子類別連儲存空間都不會繼承,因為抽象類別沒有儲存空間。要正確加入叢集,必須遵守規則。
- 自行準備儲存空間(通常是在內部持有真正的 NSArray)
- 實作所有指定的基元方法(NSArray 則是 count 和 objectAtIndex:)
其餘方法都建立在這些基元方法上,因此實作兩個方法就能啟用叢集的所有功能。不過 Effective Objective-C 的建議很明確:多數情況下,組合(持有 NSArray 的包裝類別)優於繼承。
為何 Swift 開發者也無法置身事外
Swift 的 Array 是結構,因此看似與此模式無關,但有兩個接點。
第一是橋接。Objective-C API 傳入的 NSArray 轉成 Swift Array 時,背後仍有_ContiguousArrayStorage 或__NSArrayI 等實作。追查橋接效能問題,終究會遇到這些類別叢集實作。
第二是這個想法本身延續到了 Swift。「公開型別只公開介面,實際實作交給隱藏型別」的概念,延伸為 Swift 的 AnySequence 等型別抹除包裝器,以及some關鍵字(不透明型別)。回傳型別只有一個,但實際型別只有編譯器與實作端知道,與類別叢集是同一脈絡。
總結
- NSArray・NSString・NSNumber・NSDictionary 是抽象類別,實際執行個體則是隱藏的子類別
- 型別判定務必使用 isKindOfClass:,不要比較類別是否相等
- 叢集子類別化必須實作基元方法,通常組合會是更好的選擇
- 分離介面與實作的想法,延續成 Swift 不透明型別的經典設計
即使在除錯器中遇到陌生的__NS前綴類別,也不用再慌張。那只是 Foundation 花了近 30 年打磨的最佳化,正好展現在眼前。

