Dartの同期ジェネレータの動きがさっぱりわからないので調べてみた。

先日のブログでFlutterのChipウィジェットの使い方を確認したときに出てきた以下の実装を
「同期ジェネレータ」というらしいのですがこれの動きがさっぱりわからなかったので調べてみました。

children: filterChip.toList()

Iterable<Widget> get filterChip sync* {
   for (FilterChipItemWidget filterChipItem in _filterChipItems) {
     yield FilterChip(
       backgroundColor: Colors.grey,
       label: Text(
         filterChipItem.name,
       ),
       selected: _filters.contains(filterChipItem.name),
       selectedColor: Colors.orange.shade400,
       onSelected: (bool selected) {
         setState(() {
           if (selected) {
             _filters.add(filterChipItem.name);
           } else {
             _filters.removeWhere((String name) {
               return name == filterChipItem.name;
             });
           }
         });
       },
     );
   }
 }

こちらのブログのサンプルをベースに自分的に処理の流れがわかるようにprintを調整して動かしてみました。

void main() {
  final numbers = getRange(1, 3); // 同期ジェネレータ版

  for (int val in numbers) {
    print('before print val');
    print(val);
    print('after print val');
  }
}

Iterable<int> getRange(int start, int end) sync* {
  print('called getRange()');
  for (int i = start; i <= end; i++) {
    print('before yield');
    print('yield return $i and stop\n');
    yield i; // ここまで進んで関数停止。次に呼ばれるのを待つ。
    print('after yield'); // 次に呼ばれたここから再開して次のyieldまで進む。
  }
}

出力は下記の通りです。

イテレータが呼ばれるとyieldまで進んで値を返して停止、次に呼ばれるとまたyeildまで進んで値を返して停止、を繰り返す。

最終的にイテレータメソッドgetRangeがyeildで停止せずに最後まで終了すると、呼び出し元のfor inも終了する、という流れらしいです。

>dart sync_generator.dart
# for in で1回目のgetRange(..)が呼ばれる。
called getRange()
before yield
yield return 1 and stop # ここでyieldが1を返してgetRangeが停止

# for in 内の1回目の処理が動く。
before print val
1
after print val
# for in で2回目のgetRange(..)が呼ばれる。
after yield # 停止していた場所から再開
before yield
yield return 2 and stop # ここでyieldが2を返してgetRangeが停止

# for in 内の2回目の処理が動く。
before print val
2
after print val
# for in で3回目のgetRange(..)が呼ばれる。
after yield
before yield
yield return 3 and stop # ここでyieldが3を返してgetRangeが停止

# for in 内の3回目の処理が動く。
before print val
3
after print val
# for in で4回目のgetRange(..)が呼ばれる。
after yield # getRagen(..)終了
# for in終了

ついでに、同じ動きをする自前のIteratorクラスも書いてみました。
多分同じ動きになっているはずです。

void main() {
  final numbers = GetRange(1, 3); // イテレータ版

  for (int val in numbers) {
    print('before print val');
    print(val);
    print('after print val');
  }
}

class GetRange extends Iterable<int> {
  GetRange(this.start, this.end);
  final int start;
  final int end;

  @override
  Iterator<int> get iterator => _GetRange(start, end);
}

class _GetRange implements Iterator<int> {
  _GetRange(this.start, this.end);
  final start;
  final end;
  int _current = -1;

  @override
  bool moveNext() {
    print('moveNext');
    if (_current == -1) {
      _current = start;
      return true;
    }

    if (end < _current) {
      return false;
    }

    _current += 1;

    return true;
  }

  @override
  int get current {
    print('get current $_current');
    return _current;
  }
}

出力です。

>dart sync_generator.dart
moveNext
get current 1
before print val
1
after print val
moveNext
get current 2
before print val
2
after print val
moveNext
get current 3
before print val
3
after print val
moveNext
get current 4
before print val
4
after print val
moveNext

ちょっとわかりにくいですが、左が同期ジェネレータ版、右が自前イテレータ版。
色ついてないところが、for in 内の処理部分なので、同じ結果であることがわかります。

本記事を書くために調べるまでは、呼び出し元のfor in、getRange(..)のforループ、yieldで停止してreturn、がどういう順序で実行されているのかよくわからずモヤモヤしていましたが、それがだいぶクリアになりました。

確かに、自前のIteratorクラスを書くよりも、同期ジェネレータ使ったほうがスッキリ書けそうです。
今後は使えるチャンスあったら使ってみようと思います。

ちなみに、非同期ジェネレータというものもあるらしいので後日そちらも調べてみようと思います。

参考にした記事

役に立つ記事をありがとうございます。

Dart日記30日目【ジェネレーター[同期ジェネレーター]】|まさき
同期ジェネレーター まず戻り値をIterable型にして、次に引数の後に*syncキーワードを記述 最後にreturnの代わりにyieldキーワードで戻す値を記述する Iterable<int> getRange(int start, int end) sync*{ print('called getRange()...
Dart 2 Language Guide

最後に宣伝です。

本記事がお役に立ったのであれば、自作アプリのインストールをぜひお願いします。

コメント

タイトルとURLをコピーしました