C#開発者のための最新JavaScript事情(ジェネレータ関数編):特集:C#×JavaScript(2/4 ページ)
ECMAScript 2015のジェネレータ関数とyield式を使うと、C#の反復子ブロックとyield return文と似た形ですっきりと反復処理を記述できる。
イテレータオブジェクト
ジェネレータ関数呼び出しにより返されたイテレータオブジェクトにはnextメソッドがある。これを呼び出すことで列挙を一つ進めることになる。例を以下に示す。
function* sampleGenerator(from, to) {
while(from <= to) {
yield from++;
}
}
var gen = sampleGenerator(1, 5);
var result = gen.next();
console.log("value: " + result.value);
console.log("done: " + result.done);
result = gen.next();
console.log("value: " + result.value);
console.log("done: " + result.done);
…… 省略 ……
// 出力結果
value: 1
done: false
…… 省略 ……
value: 5
done: false
value: undefined
done: true
nextメソッドの戻り値はdoneプロパティとvalueプロパティを持つオブジェクト(IteratorResultオブジェクト)であり、doneプロパティは反復処理が終了したかどうかを、valueプロパティは列挙された値を示す。上の出力結果を見ると、数値「5」までの列挙が終わると、次のnextメソッド呼び出しではdoneプロパティがtrueに、valueプロパティがundefinedになり、反復処理が完了したことが分かる。
nextメソッドを使うと、whileループを以下のように書くこともできる。ただし、ループの継続条件をnextメソッドの戻り値のdoneプロパティでチェックする上に、そのvalueプロパティを後から使おうとすると、あまりきれいな書き方にはならない(素直にfor-of文を使うのがよいだろう)。
var gen = sampleGenerator(1, 5);
var result;
while (!(result = gen.next()).done) {
console.log(result.value);
}
反復処理の実際
一つ目のES2015コードでも内部的にはnextメソッド呼び出しによって反復処理を行っている。例えば以下はnpmでbabel-cli/babel-preset-es2015/babel-polyfillパッケージを導入した環境で、一つ目のコードをJavaScript 5.1にトランスパイルした結果だ(for-ofループのみ抜粋。改行は筆者が適宜挿入。また、コードの先頭に「import "babel-polyfill";」行が必要になる)。
for (var _iterator = sampleGenerator(1, 5)[Symbol.iterator](), _step;
!(_iteratorNormalCompletion = (_step = _iterator.next()).done);
_iteratorNormalCompletion = true) {
var i = _step.value;
console.log(i);
}
深くは踏み込まないが、「sampleGenerator(1, 5)[Symbol.iterator]()」の「sampleGenerator(1,5)」は実際には上で見たsampleGenerator関数ではなく、これをラップする関数だ。参考までにBabelによるジェネレータ関数のトランスパイル結果を以下に示す。大本のジェネレータ関数のコードをトランスパイルした結果はsampleGenerator$関数に含まれている(後述)。
function sampleGenerator(from, to) {
return regeneratorRuntime.wrap(function sampleGenerator$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
from;
case 1:
if (!(from < to)) {
_context.next = 7;
break;
}
_context.next = 4;
return from;
case 4:
from++;
_context.next = 1;
break;
case 7:
case "end":
return _context.stop();
}
}
}, _marked[0], this);
}
sampleGenerator関数の実行時には大本のジェネレータ関数(sampleGenerator$関数)を利用するための設定が行われる。
ラップした側のsampleGenerator関数は「iterable」オブジェクト(イテレート可能なオブジェクト)と呼ばれるオブジェクトを返す。このオブジェクトにはSymbol.iteratorプロパティがあり、これが「イテレータオブジェクト)を返す関数」を参照している。このイテレータオブジェクトの実体は、IteratorResultオブジェクトを戻り値とするnextメソッドを持つようにラップされたジェネレータ関数だ。よって「sampleGenerator(1, 5)[Symbol.iterator]()」は全体としてはラップ後のジェネレータ関数を手に入れる処理をしていることになる。
ジェネレータ関数のトランスパイル結果についても簡単に触れておこう。この中ではwhileによる無限ループを実行しながら、反復処理を行っている。_contextのprevプロパティとnextプロパティの値は最初は0となっているので、「case 0:」節と「case 1:」節をそのまま実行する。
「case 1:」節では、最初は変数fromの値は変数toよりも小さいので、nextプロパティの値を4にしてから変数fromの値を返送する。この時点で制御はいったん呼び出し側に戻される(そして、console.logメソッドにより出力される)。
次の繰り返しでは「case 4:」節が実行され、変数fromの値をインクリメントして、nextプロパティの値を1に設定してループの先頭へと戻る。次のループの「case 1:」節では変数fromの値が変数toよりも小さいので……といった具合に処理が進められる(結果としては、ジェネレータ関数に記述した内容が実行されるのが分かるはずだ)。
難しいところはさておき、イテレート可能なオブジェクトをIEnumerable/IEnumerable<T>インタフェース(列挙可能なオブジェクト)に相当するもの、イテレータオブジェクトがIEnumerator/IEnumerator<T>インタフェース(列挙子オブジェクト)に相当するものとイメージすると理解が早いかもしれない。ちなみにジェネレータ関数が返すオブジェクトはイテレート可能なオブジェクトでもあり、イテレータオブジェクトでもある。
ジェネレータ関数自体についてはこのくらいにして、次ページではこれと一緒に使われることが多いfor-of文について見ていこう。
Copyright© Digital Advantage Corp. All Rights Reserved.