前回から少し時間が空いてしまったが、Fast Enumerationの話を続けよう。今回は、自分で作ったクラスをFast Enumerationに対応させるためのコードの書き方だ。NSFastEnumerationで定義されているメソッドを実装することになる。
NSFastEnumerationに準拠するクラス
まず、今回の説明のために一つ新しいクラスを作る。列挙可能な配列のクラスということで、EnumeratableArrayという名前にしよう。このクラスを、NSFastEnumerationプロトコルに準拠させる。
インスタンス変数として、オブジェクトの配列を持たせることにする。これは、単なるC言語の配列にしておく。これを、Fast Enumerationの文法を使ってアクセス出来るようにするのが、このクラスの目的だ。
クラスの宣言は、次のようにした。
List 1.
#define ARRAY_SIZE 32
@interface EnumeratableArray : NSObject <NSFastEnumeration>
{
id _objects[ARRAY_SIZE];
}
@end
配列の大きさは、マクロで宣言しているが、32にしている。これは、後述するが、外部バッファとして提供されるものより大きくしたためだ。
続いて、初期化メソッドを作ろう。ここでは、配列にオブジェクトを設定していく。
List 2.
- (id)init
{
self = [super init];
if (!self) {
return nil;
}
// 配列にオブジェクトを設定する
int i;
for (i = 0; i < ARRAY_SIZE; i++) {
_objects[i] = [NSNumber numberWithInt:i];
}
return self;
}
NSNumberのインスタンスを作成して、設定していく。これで準備は整った。
内部のオブジェクトを使用
では、Fast Enumerationのためのメソッドを実装しよう。今回は、二通りの実装を紹介する。内部のオブジェクトを使うものと、外部のバッファを使うものだ。
まず、内部のオブジェクトをそのまま渡すタイプのものだ。次のような実装になる。
List 3.
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state
objects:(id*)stackbuf count:(NSUInteger)len
{
// stateの値をチェックする
if (state->state >= ARRAY_SIZE) {
return 0;
}
// 配列のポインタを渡す
state->itemsPtr = _objects;
state->state = ARRAY_SIZE;
state->mutationsPtr = (unsigned long*)self;
return ARRAY_SIZE;
}
最初に、NSFastEnumerationState構造体が持つstateフィールドの値をチェックする。この値が、列挙するときの現在位置を示している。その値が配列よりも大きければ、列挙は終了だ。
列挙が可能であれば、配列の情報を設定してやる。itemsPtrフィールドには、このオブジェクトが持つインスタンス変数の配列の値を、stateフィールドには、その大きさを指定してやる。
最後に、この列挙で指定したオブジェクトの数を返してやれば、オッケーだ。これで、C言語の配列に対して、Fast Enumerationを使ってアクセス出来る。
外部バッファの利用
もう一つの方法は、外部バッファを使うものだ。先ほどのメソッドの引数として渡される、stackbufとlenを使うことになる。次のようなコードにしてみた。
List 4.
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state
objects:(id*)stackbuf count:(NSUInteger)len
{
// stateの値をチェックする
if (state->state >= ARRAY_SIZE) {
return 0;
}
// stackbufにオブジェクトを設定する
int count = 0;
while (state->state < ARRAY_SIZE && count < len) {
stackbuf[count] = _objects[state->state];
count++;
state->state++;
}
// 配列のポインタを渡す
state->itemsPtr = stackbuf;
state->mutationsPtr = (unsigned long*)self;
return count;
}
この方法では、stackbufにオブジェクトを設定していくことになる。stackbufの大きさはlenになる。さらに、設定するオブジェクトの先頭位置は、stateフィールドの値になっている。そこで、これらの値をチェックしながら、whileループを回してみた。
そして、itemsPtrフィールドに配列のポインタを渡すのだが、ここでstackbufを指定してやるのがポイントだ。これで、外部バッファを指定することになる。最後に、設定したオブジェクトの数を返してやれば完了だ。
実際にこのソースコードを走らせてみると、このメソッドが2回呼び出されることに気づく。これは、外部バッファの大きさが16であり、一回の呼び出しではすべて満たすことができないためだ。
列挙されるオブジェクトの動的な作成
外部バッファを使う方法だと、あらかじめオブジェクトを用意しておかない、ということも出来る。メソッドが呼び出されたときに、動的にオブジェクトを作成するのだ。
先ほどのList 4.を、次のように変更してみよう。
List 5.
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state
objects:(id*)stackbuf count:(NSUInteger)len
{
...
// stackbufにオブジェクトを設定する
int count = 0;
while (state->state < ARRAY_SIZE && count < len) {
// 動的にオブジェクトを作成する
stackbuf[count] = [NSNumber numberWithInt:state->state];
count++;
state->state++;
}
...
}
stateフィールドの値をもとに、随時NSNumberオブジェクトのインスタンスを作成するようにしてみた。この方法だと、あらかじめインスタンス変数としてオブジェクトの配列を確保しておく必要がなくなる。外部バッファの大きさだけ、必要なときに作成すればいい、ということになる。膨大な大きさの配列が必要なときに、有効なテクニックになるだろう。
これが、NSFastEnumerationに準拠するためのメソッドの例だ。いろいろと柔軟に対応できることが分かったと思う。データ構造によっては、標準のものを使うよりも、かなりの高速化や省メモリを実現することも出来るだろう。