検索
特集

C#開発者のための最新JavaScript事情(ジェネレータ関数編)特集:C#×JavaScript(2/4 ページ)

ECMAScript 2015のジェネレータ関数とyield式を使うと、C#の反復子ブロックとyield return文と似た形ですっきりと反復処理を記述できる。

Share
Tweet
LINE
Hatena

イテレータオブジェクト

 ジェネレータ関数呼び出しにより返されたイテレータオブジェクトには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メソッド

 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);
}

nextメソッドを利用したループ

反復処理の実際

 一つ目の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);
}

for-ofループをトランスパイルした結果

 深くは踏み込まないが、「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);
}

ジェネレータ関数の内容をラップしたsampleGnerator関数

 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.

ページトップに戻る