クラス拡張とカテゴリの違いの話。
常々「クラス拡張をカテゴリってゆーな」って思ってるんだけど、「無名カテゴリ」自体は公式の呼称なんだよね。
クラス拡張は、無名のカテゴリに似ています。が、別物です。一番の違いは、インスタンス変数を宣言できることと、宣言したメソッドはクラス本体の(=カテゴリ無しの)@implementationで実装しなければならない点です。
クラス拡張とカテゴリの一番の違いは、「クラス拡張はクラス実装と共にランタイムにロードされることが保証される」ことだと思います。
文脈として、もともとobj-c 2.0以前にはクラス拡張がなく、またメソッドのプロトタイプ宣言が必須だったこともあり、「外部に公開したくないメソッド宣言はプライベートなカテゴリを実装ファイルに宣言することで隠蔽する」というテクニックがありました。
ただしカテゴリは「実行時に 動的に クラス定義を書き換える」ものであり、「カテゴリがいつランタイムにロードされるべきか規定されていない」ため、 プライベートカテゴリは必ずしもクラス定義と共にランタイムにロードされるとは限らない 、という言語仕様上の懸念がありました。
そこでプライベートなメソッド宣言の、コンパイル時解決をサポートするために、クラス拡張が言語仕様に追加されました。上述した慣習を踏まえたために、カテゴリに極めて似通った(非常に混乱しやすい)記法が採用されたという認識を持っています。
実際のところ、Obj-Cでは実行時にクラス定義を書き換え、インスタンス変数を増やすことも可能です。ただしインスタンスのために確保されるメモリ領域も変わるため、既存のインスタンスに対して破壊的な影響を及ぼします。そのためカテゴリではインスタンス変数を宣言する操作は禁止されています。
逆説的に言えば、クラス拡張はインスタンスが一つも存在していない状態で、ランタイムにロードされることが保証されているからこそ、インスタンス変数を宣言できるのであり、この特性は主ではなく従だと思っています。
なお、現在はプライベートなインスタンス変数は@implementation
で宣言することができますので、可能な限りそうするべきです。
クラス拡張は動的にインスタンス変数を追加していない
こっから本題なのですけど、Effective Objective-Cにはこの点でひどく混乱することが書いて在ります。
それを理解するには、前提としてObjective-Cのインスタンス変数とは何かを知る必要があります。
Obj-Cにおけるインスタンス変数とは、クラスオブジェクトがobjc_ivar_list
に保持する、オフセット情報です。例えば「インスタンス変数fuga
はdouble
型で、先頭から4 byteのオフセットを持つ」といった具合で保持されています。
こうしたコードレベルでデータ型のレイアウトを規定するインターフェースのことを、ABI(アプリケーションバイナリインターフェース)と呼びます。(ABI=データ型のレイアウトではなく、ABI自体にはもっと大きな意味があります)
さて、Effective Objective-Cの「項目6 プロパティの理解」には、
オフセットは実行時にルックアップされるので、クラス定義が変わっている場合には格納されているオフセットも更新される。実行時にクラスにインスタンス変数を追加することさえできる。これはABIと呼ばれる。
そして「項目27 実装の詳細を隠すためにクラス延長カテゴリを活用せよ」には、
ABIがあるということは、オブジェクトを使うためにオブジェクトのサイズをあらかじめ知っている必要はないということだ。クラスのコンシューマがオブジェクトのレイアウトを知る必要はないので、公開インターフェイスにインスタンス変数が定義されている必要もない。そこで、クラスの実装だけでなく、クラス延長カテゴリでもクラスにインスタンス変数を追加することが可能になった。
と書いてあります。
どうも「クラス拡張によるインスタンス変数追加は、実行時に動的に行われているのだ」と読めます。
しかし、オブジェクトのレイアウトが動的でも問題がないのであれば、通常のカテゴリでも動的にインスタンス変数が追加できていいはずです。
もう一点、継承関係を持つクラスに対して、実行時にインスタンス変数を追加すると、サブクラスとの間で不整合が起きるはずです。
以下のようなクラス継承構造を考えてみます。
サブクラスで宣言されたpiyo
と、スーパークラスで実行時に動的に追加されたfoo
は同じオフセット値(アドレス)を持つことになってしまいます。
不整合を起こさないようにfoo
のオフセットをサブクラスを基準に利用されていない領域に設定することが可能なように思えますが、その時点でランタイムにロードされていない未知のサブクラスの存在を考えれば不可能ですし、それだけスーパークラスのalloc
で余分なメモリを確保することに繋がり、馬鹿げています。
また、クラスオブジェクトのオフセット値を更新すると、既に生成されてしまっているインスタンスについては不正な参照を持つことになります。
それよりも、「コンパイル時点でオブジェクトのレイアウトは決定されている」、「クラス拡張はコンパイル時点で反映される、ゆえにインスタンス変数宣言が行える」と考えた方が合理的だと思うのです。クラス拡張で宣言したメソッドを実装しないとコンパイルエラーになる性質から考えても自然です。
この点について検証してみようかなと思っていたのですが、答えは公式リファレンスにありました。
A class extension bears some similarity to a category, but it can only be added to a class for which you have the source code at compile time (the class is compiled at the same time as the class extension).
私訳: クラス拡張はカテゴリといくつかの類似性を持ちます。しかしクラス拡張はコンパイル時にソースコードが存在するクラスに対してしか追加できません。( そのクラスはクラス拡張と同時にコンパイルされます )
クラス拡張をカテゴリってゆーな
本質的な両者の違いは、
- クラス拡張は静的(コンパイル時解決)である
- カテゴリは動的(実行時解決)である
で間違いなさそうです。
カテゴリの本懐はクラス実装の分割ですが、その特性から「カテゴリ=動的な機能拡張」と考えるので、「クラス拡張=静的な機能拡張」をカテゴリの一種だと呼称するのは個人的には違和感があるのですが、公式に無名カテゴリと呼称されているので、好きなように呼べばいいと思います。
でも、「クラス延長カテゴリ」は流石にないわー…。