ここまで、Objective-C 2.0の新要素として、ガベージコレクションとプロパティと取り上げてきた。今回からは3つめの要素となる、Fast Enumerationを取り上げよう。

Fast Enumerationの書き方

「配列やリストといったコレクションの、すべての要素にアクセスするにはどうしたらいいか?」というのは、プログラミングの世界では古くからある問題だ。

C言語を振り返ってみよう。C言語には配列がある。配列にアクセスする一般的な方法は、for文と添字を使うものだ。次のような書き方になるだろう。

リスト1

int array[10];

...

int i, value;
for (i = 0; i < 10; i++) {
    // 添字を使って要素を取り出す
    value = array[i];
    ...
}

このようなfor文を使った書き方は、C言語の配列では有利だ。それは、C言語の配列は、連続したメモリブロックとして確保されるため、添字を使う場合でも速いアクセスが保証されるからだ。

だがリンクリストのようなデータ構造の場合だと、添字によるアクセスは遅くなる。そこで導入されるのが列挙子だ。列挙子は、コレクションの先頭から順々に、「その次」の要素を取り出すための仕組みだ。

Cocoa + Objective-Cでは、列挙子を表すクラスとしてNSEnumeratorを提供している。配列を表すNSArrayや辞書を表すNSDictionaryなどから、NSEnumeratorを取得することが出来る。NSEnumeratorをwhile文と組み合わせて、すべての要素にアクセスすることが出来るのだ。

リスト2

NSArray* array;

...


// 列挙子を取得する
NSEnumerator* enumerator;
enumerator = [array objectEnumerator];

// while文を使って要素にアクセスする
id object;
while (object = [enumerator nextObject]) {
    ...
}

これならばリンクリストのような構造でも速くアクセスできる。また、コレクションクラスと、そこにアクセスするためのクラスを切り分けたことで、どのようなコレクションに対しても同じ書き方が出来るという利点が生まれた。

いいことづくしのように見えるNSEnumeratorだが、その欠点はソースコードの記述量が多いことにある。NSEnumerator型の変数を1個余分に宣言しなくてはいけないし、なによりこの「NSEnumerator」というクラス名が長い。プログラミングの最中では頻繁に出てくる書き方なので、煩わしさを感じることもある。

そこでObjective-C 2.0から導入されたのが、Fast Enumerationだ。これは列挙子をより使いやすくするためのものだ。文法としては、for文を拡張している。いろいろ説明するよりも、見てもらった方が早いだろう。NSArrayの要素にアクセスする、Fast Enumerationとfor文を使ったソースコードは、次のようになる。

リスト3

NSArray* array;

...

// for文を使ってNSArrayの要素にアクセスする
for (id object in array) {
    ...
}

こういうことだ。C言語の書き方に近い。とても直感的ではないだろうか。

for文の中を見てみると、先頭に一時変数の宣言があり、その後ろにinというキーワードがある。このinの後ろに、配列などのコレクションクラスを置くのだ。

列挙子の利点を活かしながら、記述するソースコードの量を確実に減らす。それがFast Enumerationの目的だ。

逆方向へのアクセス

Fast Enumerationで、for文のinの後ろに置くことが出来るのは、NSArrayだけではない。実は、NSEnumeratorを置くことも出来るのだ。これを活用して、NSArrayの先頭からだけではなく、最後尾から先頭に向かってアクセスするようにすることも出来る。

どうするかというと、NSArrayには列挙子を取得するメソッドとしてobjectEnumeratorがあるが、もう1つreverseObjectEnumeratorというメソッドもある。これを使うと、名前から予想がつくと思うが、順番をひっくり返してアクセス出来るのだ。

reverseObjectEnumeratorが必要になるのは、配列から要素を削除するときだ。たとえば、配列からある特定の要素を削除する、という処理を考えてみよう。単純に書くと、こうなる。

リスト4

for (id object in array) {
    // objectを削除する必要があるかどうかチェックする
    if ([object shoudRemove]) {
        // objectを削除する
        [array removeObject:object];
    }
}

このソースコードには問題がある。実際に走らせてみると、「Collection was mutated while being enumerated.」という例外が発生する。つまり、「列挙子を用いてアクセスしている最中に、配列の中身を変更するな」ということだ。

これは、配列の構造を考えればわかる。配列の要素を削除すると、それより後ろにある要素すべてが、「1つ前」へ移動することになる。「その次」の要素をたぐっていくNSEnumeratorにとっては、都合が悪い。

この問題を回避するには、1回配列をコピーして、列挙子を取り出す配列と削除を行う配列とを分ける、という方法もあるが煩わしい。

そこで使うのが、reverseObjectEnumeratorだ。これを使って、逆向きに走査を行えばいい。

リスト5

// 逆方向にアクセスする
for (id object in [array reverseObjectEnumerator]) {
    if ([object shoudRemove]) {
        [array removeObject:object];
    }
}

この書き方だと、先ほどの例外は発生しない。要素を削除して配列の状態が変わったとしても、これからアクセスする要素には影響がないからだ。これで要素の削除も、効率的に記述することができる。

今回はFast Enumerationの使い方について説明した。次回は、その実装の仕組みに踏み込んでみよう。