【Dart】コレクション(List,Set)で計算結果が最小・最大になる要素を取得

  • 2025年7月12日
  • 2025年7月12日
  • Dart

対象読者

  • Dart / Flutter 初〜中級開発者
  • コレクション操作やパフォーマンス最適化に興味があるエンジニア
  • 「計算結果に関連する最小(あるいは最大)値と その要素」を一発で取り出したい人

はじめに

「複数プランの試算結果を比較し、最も安いプランを選びたい」という業務があったので、それのテスト実装をしました。そのまとめです。
単に 最小値 を知りたいだけなら minreduce で十分ですが、その値を生み出した要素 も欲しいとなると少し工夫が必要でした。

計算コストが低いケース

reduce で最小値を取得する

final fruits = {'apple', 'banana', 'grape'};
final result = fruits.reduce(
  (prev, curr) => prev.length < curr.length ? prev : curr,
);
expect(result, 'grape');

ポイント

  • コレクションが Set のため、順序保証は不要と仮定。
  • reduce要素を 1 → 1 へ畳み込む シンプル API。
    ただし 空集合 だと StateError が発生する点に注意。
  • 処理内容は「prev と curr のどちらが短いか」だけ。
    最小値を返すだけなら実装も読みやすい。

使いどころ

  • だけ欲しいとき。
  • データ件数が数十〜数百で、計算コストがほぼゼロ のとき。

fold で最小要素を取得する

final fruits = {'apple', 'banana', 'grape'};
final result = fruits.fold<String>(
  fruits.first,                          // 初期値
  (prev, curr) => prev.length < curr.length ? prev : curr,
);
expect(result, 'grape');

なぜ fold?

  • 初期値を自由に渡せる→ 空集合でも安全。
  • 返り値の型をカスタムできる→ ここでは String のまま。

ここが reduce と違う

  • fold畳み込み結果の型を変えられるT → S)。
    「要素 & 計算済みの値」を一緒に持ちたい場合、後述のコードで真価を発揮。

計算コストが大きいケース

上記を実施して、「計算を2回やっていて、あまり良くないなぁ」と思いました。そこで、次の計算に前の結果を持ち越せるように工夫してみました。

fold+タプルで計算結果を持ち越し

final fruits = ['apple', 'banana', 'grape'];

final firstElement = (fruits.first, fruits.first.length);  // (要素, 集計値)
final (selectedElement, selectedLen) =
    fruits.skip(1).fold<(String, int)>(firstElement, (prevData, newElem) {
  final (prevElem, prevLen) = prevData;
  final newLen = newElem.length;           // ▶︎ 集計はここだけ!

  return prevLen < newLen ? prevData : (newElem, newLen);
});

expect(selectedElement, 'grape');
expect(selectedLen, 5);

なぜタプル?

  • 1回の走査で「要素」と「計算済み値」両方を保持できる。
  • 既に求めた prevLen を再計算しなくて済む → 計算コスト半減
  • Dart 3 のレコード(タプル)構文で簡潔に書ける。

具体的なユースケース

複数の料金プランを走査し、最も安いプランを表示させるときに使用しました

fold+nullable で初期化を遅延

「初期値をnullにするいいよ」というChatGPTの提案にしたがって、実験。nullable関連の「?」「!」の場所が結構ややこしかった。

final fruits = ['apple', 'banana', 'grape'];

final (selectedElement, selectedLen) =
    fruits.fold<(String, int)?>(null, (prevData, newElem) {
  if (prevData == null) {
    // 初回のみ初期化
    return (newElem, newElem.length);
  }

  final (prevElem, prevLen) = prevData;
  final newLen = newElem.length;

  return prevLen < newLen ? prevData : (newElem, newLen);
})!;

expect(selectedElement, 'grape');
expect(selectedLen, 5);

何が嬉しい?

  • 初期値を作るロジック を fold 内に閉じ込められる。
  • 前項目で最初の項目を使ったのでskip(1)を書いたが、それがない。代わりに、fold文の最後に })!;で「!」がないとコンパイルエラー。

参考

【Dart】Set, List, Mapの操作法を徹底解説!メソッド網羅!

まとめ

コレクションの中で結果が最小・最大になる値を取得する、というケースはよくあります。reduleでサクッと書いてしまおうかとも思いましたが、計算結果が重めなので、キャッシュした方が良いかな、とちょっと考えて見ました。