Swift & Objective-C

クラスクラスタ: NSArrayが抽象クラスである理由

Objective-Cでこのコードを実行すると、かなり意外な結果になります。

読了 4 分
クラスクラスタ: NSArrayが抽象クラスである理由のカバー画像

Objective-Cでこのコードを実行すると、かなり意外な結果になります。

NSArray *array = [NSArray arrayWithObjects:@1, @2, nil];
NSLog(@"%@", [array class]);
// 出力: __NSArrayI

確かにNSArrayを作ったのに、クラス名は__NSArrayIです。空配列なら__NSArray0、要素が1つなら__NSSingleObjectArrayIになることもあります。

バグではありません。これが**クラスクラスタ(Class Cluster)**パターンです。

NSArray、NSString、NSNumber、NSDictionaryはすべて抽象クラスです。

実際のインスタンスは、状況に応じた非公開サブクラスとして生成されます。

Effective Objective-C 2.0の項目9で扱われる内容で、Foundationを使う限り避けられない構造です。


まずは要点(3つ)

  1. NSArrayなどのFoundationコレクションは、公開インターフェースだけを提供する抽象クラスです。
  2. イニシャライザが非公開サブクラスのインスタンスを選んで返します(ファクトリーパターンの一種)。
  3. そのため、[array class]比較は危険で、サブクラス化にも厳しい規則があります。

なぜこの設計なのでしょうか?

1つの配列にも複数の最適化戦略があります。

  • 要素0個の不変配列: 毎回作らず、シングルトン1つで十分
  • 要素1〜2個: ポインターを構造体にインライン化する方が高速
  • 大きな可変配列: 再割り当て戦略を持つ別実装が必要

これらをNSArrayの実装にif文で詰め込む代わりに、Foundationは状況別の専用サブクラスを用意し、ファクトリーメソッドが適切なものを返します。利用側はNSArrayのインターフェースだけ知ればよく、内部実装はOSごとに自由に差し替えられます。

実際に[NSArray alloc]が返すのは__NSPlaceholderArrayという仮オブジェクトで、続くinit系の呼び出しで本物の実装に変わります。allocとinitを慣例的に続けて使いますが、クラスクラスタではこの2段階で別のオブジェクトへ移ります。

ファクトリーが要素数を見て実装を選びます
ファクトリーが要素数を見て実装を選びます

実務で踏みやすい2つの落とし穴

落とし穴1: クラスを直接比較する

// 危険なコード
if ([object class] == [NSArray class]) {
    // ここには決して入りません。実際のクラスは __NSArrayI などだからです.
}

// 正しいコード
if ([object isKindOfClass:[NSArray class]]) {
    // クラスタ全体を含めて判定します
}

クラスクラスタのメンバーは公開クラスのサブクラスなので、同一性比較ではなくisKindOfClass:で系統を確認します。

落とし穴2: 安易なサブクラス化

NSArrayを継承してメソッドを1つ上書きすればよさそうですが、そのサブクラスは保存領域すら継承しません。抽象クラスにはストレージがないためです。クラスタに正しく組み込むには規則に従う必要があります。

  • 独自の保存領域を用意する(通常は内部に本物のNSArrayを保持する)
  • 指定されたプリミティブメソッドをすべて実装する(NSArrayならcountとobjectAtIndex:)

残りのメソッドはすべてこのプリミティブ上で動くため、2つを実装すればクラスタの全機能が使えます。ただしEffective Objective-Cの助言は明快です。多くの場合、継承よりコンポジション(NSArrayを保持するラッパークラス)が適しています。


Swift開発者にも無関係ではない理由

SwiftのArrayは構造体なので無関係に見えますが、接点は2つあります。

1つ目はブリッジングです。Objective-C APIから来たNSArrayがSwift Arrayに変わるときも、その背後では_ContiguousArrayStorageや__NSArrayIなどの実装が動いています。ブリッジングの性能問題を追うと、結局これらに行き着きます。

2つ目は発想そのものがSwiftに受け継がれていることです。「公開型はインターフェースだけを公開し、実装は隠れた型が担う」という考えは、SwiftのAnySequenceのような型消去ラッパーやsomeキーワード(不透明型)につながります。返り値は1つでも実際の型はコンパイラーと実装だけが知る構造で、クラスクラスタと同じ系譜です。

デバッガーに表示される__NSクラスこそ、その正体です
デバッガーに表示される__NSクラスこそ、その正体です

まとめ

  • NSArray・NSString・NSNumber・NSDictionaryは抽象クラスで、実際のインスタンスは非公開サブクラスです
  • 型判定には必ずisKindOfClass:を使い、クラスの同一性比較は避けます
  • クラスタのサブクラス化にはプリミティブメソッドの実装が必須で、通常はコンポジションが適しています
  • インターフェースと実装を分離する発想は、Swiftの不透明型へ続く由緒ある設計です

デバッガーで見慣れない__NS接頭辞のクラスに出会っても、もう慌てる必要はありません。Foundationが約30年かけて磨いてきた最適化が目の前に現れただけです。