objc_enumerationMutationに渡される引数

まず前回の記事の補足から入ろう。前回、コンパイラが生成するFast Enumerationのためのコードを紹介したとき、「objc_enumerationMutation(id)に渡される引数の値がよくわからない」と書いた。記事の掲載後、読者の方から「そこに指定されるのはcollectionではないか」という指摘をいただいた。この方は、アセンブラコードを直接表示して解釈したようだ。

そのときのやり取りを、一部引用させていただく。

XCodeメニュー[ビルド]→[アセンブラコードの表示]で、Fast Enumerationのコードを直接見ることができます。それによると、objc_enumerationMutation(id)に渡されるパラメータは、collectionでした。ちなみに、objc_enumerationMutation(id)は例外出力処理しかやってないみたいです。渡されたパラメータも例外メッセージの出力に利用されてるだけです。ループ全体を@try~@catchで囲めば、例外を捕捉できます。
(引用元はこちら)

このようなことなので、objc_enumerationMutationに渡されるのはcollectionのようだ。指摘していただいた、しましまさんには感謝させていただく。

Fast Enumerationの流れ

では、あらためてFast Enumerationのためのソースコードを眺めてみよう。ここでは、for文を展開したソースコードを持つ側をクライアント、呼び出されるコレクションクラスの方をレシーバと呼ぶことにする。前回の記事のソースコードを参照しながら読んでほしい。

まず、NSFastEnumerationStateの変数を確保し、フィールドをすべて0に設定する。このとき大事なのは、この構造体が持つstateフィールドが0になることだ。これは状態が初期状態であることを示す。

次に、1回目のcountByEnumeratingWithState:objects:count:の呼び出しが発生する。ここでレシーバは、列挙されたオブジェクトとその数を返すことになる。列挙されたオブジェクトは、NSFastEnumerationStateのitemsPtrフィールドに、id型の配列として指定される。配列の要素数は、メソッドの返り値になる。

このとき、列挙されたオブジェクトの数は、必ずしもすべてのオブジェクトの数になるわけではないことに注意してほしい。一度にすべてのオブジェクトを列挙することもできるが、何回かに分けて渡すこともできるのだ。これは、すべての列挙に時間がかかるようなデータ構造のときに有効だろう。

また、レシーバは同時に、NSFastEnumerationStateのstateの値を設定することもできる。これはレシーバで使うことのできる、状態を表す値となり、次回の呼び出しのときに活用できるだろう。クライアント側ではこの値は参照しないようだ。

列挙されたオブジェクトの配列とその数を得たら、do-whileループを回して、オブジェクトを1つずつ取り出す。つまり、itemsPtrフィールドから取得するのだ。そして、for文の中に書かれた処理を繰り返す。

すべてのオブジェクトを取り出したら、再びcountByEnumeratingWithState:objects:count:を呼び出す。レシーバ側は、NSFastEnumerationStateのstateの値を見て、この呼び出しがどういう状態で行われたのかを判断出来るだろう。すべてのオブジェクトの列挙が終わっていれば、0を返す。まだ残っているオブジェクトがあるのであれば、それを返して、再びループの中に入るだろう。

これが、Fast Enumerationのクライアント側の挙動になる。

列挙するオブジェクトのためのバッファ

先ほどの説明では、レシーバ側はcountByEnumeratingWithState:objects:count:メソッドの引数を、1つしか使わなかったことに気づいたであろうか。第1引数である、NSFastEnumerationState型のものである。第2引数と第3引数は、どのように使うのであろうか。

これは、列挙したオブジェクトをどのようにクライアント側に渡すのか、ということに関わってくる。列挙したオブジェクトは、id型の配列の先頭アドレスという形で渡すのだが、一番簡単なのは、レシーバ側ですでにそのような配列を持っているケースだ。この場合は、そのままその配列を渡してしまえばいい。

だが、複雑なデータ構造では、そのような都合のいい配列がないこともあるだろう。その場合、どこかに配列を確保する必要があるが、レシーバの内部で確保することは避けたい。なぜなら、NSFastEnuemrationで定義されているプロトコルは1つだけで、列挙が終わった後に呼び出されるものはない。ということは、確保した配列を解放するタイミングがないのだ。

そこで提供されるのが、先ほど触れた、countByEnumeratingWithState:objects:count:メソッドの第2、第3引数である、stackbufとlenだ。これらは、列挙したオブジェクトを格納する配列として使えるバッファなわけだ。レシーバ側は、オブジェクトのアドレスをこのバッファにコピーして、itemsPtrとしてこのバッファを指定してやればいい。

クライアントのソースコードを見てみると、ここに渡される引数は、スタック上に置かれた、長さ16の配列ということになっている。長さ16という値はコンパイルのバージョンによって変わるかもしれないが、何にせよ、レシーバ側で自由に使えるバッファを確保しているわけだ。

ここまでが、Fast Enumerationのために作られたソースコードの解説だ。次回は、レシーバ側でcountByEnumeratingWithState:objects:count:メソッドをどのように実装するのか、見てみよう。