🐘

dartのリストでよく利用する機能のExtension化

2022/05/15に公開約4,200字

筋トレの記録アプリFlutterで実装していてリスト操作することがあったり、リストから最大値を求めるようなことがよくあったりするためIterableextension として👇のようなものを定義してます。

extension IterableExtension<E> on Iterable<E> {
  /// Iterableをmapしながら indexも返す
  Iterable<T> mapIndex<T>(T Function(int i, E e) action) {
    var i = 0;
    return map((e) => action(i++, e));
  }

  /// IterableをforEachでindexも返す
  void forEachIndex(Function(int i, E e) action) {
    toList().asMap().forEach((i, e) => action(i, e));
  }

  /// 各要素のmaxを返す。maxの判定は各[element]のgetValueで判定する。
  E? max(num Function(E element) getValue) {
    if (isEmpty) return null;

    return reduce((a, b) {
      final aValue = getValue(a);
      final bValue = getValue(b);
      if (aValue > bValue) return a;
      if (aValue < bValue) return b;

      return a;
    });
  }

  /// 各要素のmaxをElementWithIndex(elementとindexを持つクラス)で返す。
  /// maxの判定は各[element]のgetValueで判定する。
  ElementWithIndex<E>? maxWithIndex(num Function(E element) getValue) {
    if (isEmpty) return null;

    return mapIndex((i, e) => ElementWithIndex<E>(i, e)).reduce((a, b) {
      final aValue = getValue(a.element);
      final bValue = getValue(b.element);
      if (aValue > bValue) return a;
      if (aValue < bValue) return b;

      return a;
    });
  }
}

class ElementWithIndex<E> {
  const ElementWithIndex(this.index, this.element);

  final int index;
  final E element;
}

それぞれなぜ定義しているかを下記してみます。

mapIndex, forEachIndex

👇の記事を参考にさせていただいてます。

https://qiita.com/kazy_dev/items/bc081ea9b711c26957f6

example

void main() {
  final List<String> names = ['Jone', 'Ken', 'Taro'];
  names.forEachIndex((i, e) {
    print('${i}: ${e}');
  });
  /**
  0: Jone
  1: Ken
  2: Taro
  */
  
  final list = names.mapIndex((i, e) => ElementWithIndex(i, e)).toList();
  list.forEach((e) {
    print('${e.index}: ${e.element}');
  });
  /**
  0: Jone
  1: Ken
  2: Taro
  */
}

forEachIndextoList().asMap().forEach() でも書けるもののあった方が便利な気はしています。
mapIndexは、toList().asMap().map(MapEntry<K2, V2> convert(K key, V value)) だと mapのconvertでMapEntryを返さなければならなくて、代替ができないため、あった方が便利だと感じています。

max

dartには標準で ListIterable に max関数が無い(と思っている)のと、あるクラスの特定のプロパティでのmaxを取得したいと言うのがよくあるため、extensionで定義してます。

max関数自体はdartに標準で存在しているのですが、これは2つの値のうち大きいものを返す、というものです。そのため、list.reduce(max) みたいな感じで最大値取得を書くケースは良くあると思います。
標準のmax関数 の定義はT max<T extends num>(T a, T b);であり、標準でlist.reduce(max) を利用する場合は比較する値が数値である必要があるため、自分は上記の通り要素がクラスでその要素のプロパティでのmaxを取得したい、というのがあるため、maxメソッドをextensionに定義しています。

example

class TrainingSet {
  const TrainingSet(this.weight, this.note);

  final double weight;
  final String note;
}

void main() {
  final List<TrainingSet> trainingSets = [
    TrainingSet(20, 'ベンチプレス1'), 
    TrainingSet(30, 'ベンチプレス2'),
    TrainingSet(15, 'ベンチプレス3'),
  ];

  final maxSet = trainingSets.max((e) => e.weight);
  print(maxSet!.note);
  // ベンチプレス2
}

maxWithIndex

リスト内の最大値をもつ要素を取得するのは、👆のextensionで定義した max でできるのですが、maxを取得した上で、その要素のindexも取得したいこともわたしはよくあります。
これは要素自体を取得できれば ListindexOfでも取得できるのですが、リストの要素が freezedのクラスになっている場合に、その要素の等価かどうかが 全てのプロパティが一致しているかどうかでの判定になるため、それによる誤動作があったらちょっと嫌だなと言う思いで書いてます。
この最大値取得の処理ではそれ(freezedの等価判定による影響)による誤動作は無いと思うのでindexOfで問題無いとは思うのですが、リストの並べ替えを実装している時にはfreezedの等価判定に依存する問題に直面していました。
それが嫌だったので、maxWithIndex をextensionに定義しているため、定義したモチベーションとしては独自性が強いと思っています。

freezedのクラスの等価については下記でまとめています。

https://zenn.dev/hs7/articles/243f7193f351ea

Example


class TrainingSet {
  const TrainingSet(this.weight, this.note);

  final double weight;
  final String note;
}

void main() {
  final List<TrainingSet> trainingSets = [
    TrainingSet(20, 'ベンチプレス1'), 
    TrainingSet(30, 'ベンチプレス2'),
    TrainingSet(15, 'ベンチプレス3'),
  ];

  final maxSetWithIndex = trainingSets.maxWithIndex((e) => e.weight)!;
  print(maxSetWithIndex.element.note);
  print(maxSetWithIndex.index);
  // ベンチプレス2
  // 1
}

Discussion

ログインするとコメントできます