Swift & Objective-C

Objective-Cのid型を完全攻略(動的型付け・ダックタイピング総まとめ)

Objective-Cを初めて学ぶと、idという型の前で一度は戸惑うものです。

読了 5 分
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という1つのポインターを持つ構造体へのポインターです。

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]を呼び出しても、コンパイル時にはobjlengthに応答するか分かりません。

実行時にobjのクラスを調べて該当メソッドを探し、実行します。これをメッセージディスパッチと呼びます。

比較すると次のようになります。

  • 静的型付け:コンパイラーが型を事前に確定して検査
  • 動的型付け:実行中に実際のオブジェクトを見て判断

Objective-Cでは、idと動的型付けが「まず入れて、本当の正体は実行時に尋ねる」という思想を形にしています。


ダックタイピングの本当の意味とは?

ここは多くの人が誤解する点です。ダックタイピングを単に「型検査をしないこと」と捉えてしまうのです。

ダックタイピングの元になった表現は次のとおりです。

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」

オブジェクトがどのクラスに属するかではなく、「そのメッセージに応答できるか」が重要だという意味です。

Objective-Cでは、respondsToSelector:でこれを直接確認できます。

// クラスに関係なく、このメソッドに応答すれば呼び出す
if ([obj respondsToSelector:@selector(quack)]) {
    [obj quack];  // アヒルのように鳴ければアヒルとして扱う
}

ここではobjDuckクラスかRobotクラスかは関係ありません。

quackというメッセージに応答すれば、コード上は「アヒル」です。

これは継承ベースのポリモーフィズムとは性質が異なります。

同じ親クラスを継承していなくても、無関係なクラスでも同じメソッド名さえ持てば同じように扱えるからです。

この柔軟性が、デリゲートパターンやターゲット・アクションなど、Cocoaの中核設計を可能にした土台です。

メソッドに応答するだけでアヒルとして認める発想は面白いですよね
メソッドに応答するだけでアヒルとして認める発想は面白いですよね

便利な分だけ危険なid、いつ注意すべきか

idは自由である分、代償も伴います。

コンパイラーが型検査をほとんど行わないため、応答できないメッセージを送ってもコンパイルは通り、実行時にアプリが落ちます。

あの有名なunrecognized selector sent to instanceクラッシュです。

そのため、実務では次のように使います。

  • 型が確実なら、idではなくNSString *など具体的なクラスを明示する
  • どうしてもidを使うなら、呼び出し前にrespondsToSelector:で防御する
  • デリゲートプロトコルを定義し、「このメッセージに応答する」ことを文書化する

最近はinstancetypeNSArray<NSString *> *のようなジェネリクスが導入され、無闇なidの使用は大きく減りました。

それでもフレームワークの基盤やランタイムを扱うコードでは、idが今も心臓のように動いています。

この赤いログは、idを誤用したときに最もよく出会う顔です
この赤いログは、idを誤用したときに最もよく出会う顔です

まとめると、idはObjective-Cの動的な性質を象徴する型であり、動的型付けとダックタイピングは「クラスではなく振る舞いでオブジェクトを判断する」という一つの思想につながっています。

最初は unfamiliarで危険に見えても、この原理を理解すればCocoaフレームワークの設計がずっと明確に見えてきます。

今日迷ったidの前で、少しでも戸惑いが減っていれば幸いです。楽しいObjective-C学習を!