NSFastEnumeration で for...in 構文に対応させる : Objective-C プログラミング

PROGRAM


NSFastEnumeration で for...in 構文に対応させる

Objective-C では、クラスを NSFastEnumeration プロトコルに準拠させることで、そのインスタンスを for ... in 構文で使うことができるようになります。

// @interface で NSFastEnumeration プロトコルに準拠することを明示します。

@interface EzSampleList : NSObject <NSFastEnumeration>

このようにインターフェイス部分で NSFastEnumeration プロトコルを指定することで、自作のクラスも次のように for ... in 構文で使用できるようになります。

// NSFastEnumeration に準拠することで、お馴染みの for ... in 構文で使用できます。

EzSampleList* myList = [[EzSampleList alloc] init];

 

for (id item in myList)

{

}

もちろんこれが正しく動作するためには、適切なリストを返す処理を実装する必要があります。

NSFastEnumeration では "-countByEnumeratingWithState:objects:count:" というメソッドひとつを実装して、for ... in 構文でひとつひとつ取り出せるインスタンスのリストを返す処理を書くことになります。

 

NSFastEnumeration プロトコルに準拠したインスタンスが for ... in に渡されると、列挙されるインスタンスのうちのいくつかを返す -countByEnumeratingWithState:objects:count: メソッドが、必要な回数だけ呼び出される仕組みになっています。

このメソッドには for ... in の呼び出し元との情報を交換するための構造体が渡されるので、そこに列挙したいインスタンスの一覧と、どこまで列挙したかなどの状態を保存して、列挙したアイテムの数を返すようにします。

 

なお、クラスを for ... in に対応せる別の方法として NSEnumerator を使う方法もあります。

今回の NSFastEnumeration プロトコルを使う方法だと目的のクラスに直接処理を実装できるため、インスタンス変数や内部メソッドを効率よく使って、より高速な列挙処理を実装することも可能です。

標準的な実装

たとえば、NSArray 型のインスタンス変数 _list が持っている値を for ... in で順番に取り出す場合について見てみます。

 

列挙したいインスタンスは -countByEnumeratingWithState:objects:count: メソッドが呼び出されたときに、渡された構造体を通して返します。

このとき、列挙するインスタンスはいくつかにまとめて配列で返す必要がありますが、そのときに使用する配列は、引数で受け取ったメモリに格納して返すこともできますし、独自にメモリを確保してそれを返すことも出来るようになっています。

今回は、引数で受け取ったメモリ (buffer) を使って返してみます。

 

なお、引数に渡される state->state では、何番目まで列挙したかを管理します。

初回呼び出し時は 0 が、2 回目以降の呼び出しでは前回ここに設定した値が渡されるので、これを使って次に何を返せばいいかを判断できます。

// インスタンス変数 _list が持っている値をそのまま for ... in で取り出せるようにします。

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)len

{

NSUInteger bufferIndex = 0;

 

// state->state には次の列挙位置(初回呼び出し時は 0)が入っているので、それを使います。

NSUInteger listIndex = state->state;

NSUInteger listLength = _list.count;

 

// 引数に渡されたバッファーのサイズを上限に、インスタンスをバッファーに列挙します。

while (bufferIndex < len)

{

// ただし、保持しているリストを全て列挙できたら終了します。

if (listIndex >= listLength) break;

 

// バッファーの現在位置にリストの要素をひとつ設定します。設定後にそれぞれの位置を進めています。

buffer[bufferIndex++] = _list[listIndex++];

}

 

// 引数に渡された state に、呼び出し元に伝える情報を設定します。

state->state = listIndex;

state->itemsPtr = buffer;

state->mutationsPtr = (unsigned long*)(__bridge void*)self;

 

// 列挙できたインスタンスの数を返します。

// この段階では bufferIndex に (インデックス番号 + 1) が入っているので、つまり列挙できた個数になっています。

return bufferIndex;

}

今回は連続した配列の値をそのまま返しているだけなので簡単ですけど、逆順で返したいとか、何か処理を加えて列挙したいという場合でも、このメソッドの中で自由にプログラムを組むことができます。

列挙した値は、最終的に state 構造体にセットして、その個数を return で返します。

state->state
(unsigned long)
どこまで列挙が進んだかの状態をセットします。ここにセットした値は2回目以降の呼び出し時にまた渡されるので、続きから列挙することができます。
state->itemsPtr
(id*)
列挙したインスタンスを格納する C 配列をセットします。引数として渡された buffer をそのまま使えば、メモリ管理を呼び出し元任せにできて簡単です。固定配列や malloc 等で自前で用意したものをセットすることもできます。
state->mutationsPtr
(unsigned long*)
列挙処理の最中で、ここで指定したポインタが指す unsigned long の値が変化すると、NSGenericException 例外 (*** Collection was mutated while being enumerated.) が発生します。列挙中に値が変化したことを検出して中断させる必要がある場合に活用できます。

値の変化を検出する必要がない場合でも NULL を渡すとエラーになるので、そのときには自分のポインタ (self) を unsigned long* にキャストして渡せば大丈夫です。(iOS 6.0 では *id と unsigned long のサイズが一緒ですし、self の中身は Class なので問題なさそうです)
state->extra[5]
(unsigned long)
列挙する際に使用する情報をここに保存しておけます。使わなくても問題ありません。ここにセットした値は2回目以降の呼び出し時にまた渡されます。
return state->itemsPtr にセットしたインスタンスの個数は、戻り値として返します。0 を返した時点で列挙終了とみなされます。

これらの構造体の値は、最初の -countByEnumeratingWithState:objects:count: メソッド呼び出し前に 0 で初期化された後、呼び出し元で変更されることはないようなので、更新の必要がない項目は、最初の 1 回(state->state == 0 のとき)だけ設定すれば大丈夫です。

引数に渡されるバッファー(buffer)も複数回呼ばれるときには毎回同じ場所が渡されるようなので、最初の 1 回(state->state == 0 のとき)だけ itemsPtr に設定すれば良さそうでしたが、引数から毎回受け取るものなので、今回の例では念のためその都度、新しく渡されたバッファーを使うことにしてみています。

目的のインスタンス変数が NSFastEnumeration を実装している場合

列挙したいインスタンスが NSArray のような、既に NSFastEnumeration に準拠しているものの場合、それが持つ -countByEnumeratingWithState:objects:count: メソッドを呼び出してあげることでも実装できます。

// 列挙したいインスタンスが、内部で既に NSFastEnumeration によって管理されている場合は簡単に実装できます。

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)len

{

return [_list countByEnumeratingWithState:state objects:buffer count:len];

}

このように、列挙したいインスタンスを保持しているインスタンス変数(ここでは _list)の -countByEnumeratingWithState:objects:count: メソッドにそのまま引数を中継することで、内包する NSFastEnumeration の列挙をそのまま拝借できます。

[ もどる ]