🎯

リストをmutateさせる処理を書く際の注意点

2024/01/20に公開

この記事は何?

この記事は、Flutterであるリストに対して要素を追加したり削除したりする(=mutate操作)処理を実装する際に注意すべき点について例を提示しながら説明を行う記事です。

この記事が対象としている読者

  • Flutterを使い始めてまだ日が浅い開発者
  • リストに対してmutate操作を行うようなUtil関数を実装する際に気をつけることが知りたい開発者

結論

要素を追加・削除などする場合には新しいリストに詰め替えをするような実装になっているかに注意する必要があります.
こういった実装にしていないと、mutate操作ができないリストを渡された際にエラーを吐いてしまうためです。

// 省略

List<LikeArticle> register(List<LikeArticle> base, int articleId) {
  final target = LikeArticle(
    articleId: articleId,
    likedAt: DateTime.now(),
  );

  // ng
  // base.add(target);
  // return base;

  // better
  return [
    ...base,
    target,
  ];
}

List<LikeArticle> unregister(List<LikeArticle> base, int articleId) {
  // ng
  // base.removeWhere((e) => e.articleId == articleId);
  // return base;

  // better
  return [
    ...(base.where((e) => e.articleId != articleId)),
  ];
}

// 省略
コード全文
import 'dart:collection';

class LikeArticle {
  const LikeArticle({
    required this.articleId,
    required this.likedAt,
  });

  final int articleId;
  final DateTime likedAt;
}

List<LikeArticle> register(List<LikeArticle> base, int articleId) {
  final target = LikeArticle(
    articleId: articleId,
    likedAt: DateTime.now(),
  );

  // // NG
  // base.add(target);
  // return base;

  // better
  return [
    ...base,
    target,
  ];
}

List<LikeArticle> unregister(List<LikeArticle> base, int articleId) {
  // // NG
  // base.removeWhere((e) => e.articleId == articleId);
  // return base;

  // better
  return [
    ...(base.where((e) => e.articleId != articleId)),
  ];
}

void main() {
  final normal = <LikeArticle>[];
  final unmodifiable1 = List<LikeArticle>.unmodifiable([]);
  final unmodifiable2 = UnmodifiableListView(<LikeArticle>[]);

  register(normal, 100);
  register(unmodifiable1, 100);
  register(unmodifiable2, 100);

  unregister(normal, 100);
  unregister(unmodifiable1, 100);
  unregister(unmodifiable2, 100);
}

何を考慮する必要があるのか?

以下のようなリストに対してmutate操作を行おうとしていないかを考慮する必要があります。

  • 固定長リスト
    • 例: List.filled()
  • unmodifiableなリスト
    • 例: UnmodifiableListView(), List.unmodifiable()

補足: リストの種類

Dartではリストとして、固定長リスト(= Fixed-length list)と可変長リスト(= Growable list)の2種類があります。

固定長リスト

  • 長さを変更することはできない。
  • 長さを変更するような操作はエラーになる。
  • 要素へのアクセスや変更は可能だが、全体のサイズは一定。
// 5つの要素(0)を持つ固定長のリストを作成
final fixedLengthList = List<int>.filled(5, 0);
print(fixedLengthList); // [0, 0, 0, 0, 0]

// リストの長さを変更しない処理はできる
// 例: 特定の要素の値を変更する
fixedLengthList[0] = 87;
fixedLengthList.setAll(1, [1, 2, 3]);
print(fixedLengthList); // [87, 1, 2, 3, 0]

// リストの長さが変わるような処理をしようとするとエラーを吐く
// 例: 要素を追加・削除する
fixedLengthList.add(499);

可変長リスト

  • サイズを変更できる。
  • 要素の追加や削除が可能。
  • 内部バッファを保持し、必要に応じてそれを増やすので、効率的に要素を追加できる。
// 5つの要素(0)を持つ可変長のリストを作成
final growableList = List<int>.filled(5, (_) => 0);
print(growableList); // [0, 0, 0, 0, 0]

// リストの長さを変更しない処理はできる
// 例: 特定の要素の値を変更する
growableList[0] = 87;
growableList.setAll(1, [1, 2, 3]);
print(growableList); // [87, 1, 2, 3, 0]

// リストの長さが変わるような処理はできる
// 例: 要素を追加・削除する
growableList.add(499);

何も意識しなければ大抵のリストは可変長のはず

補足: UnmodifiableListView() と List.unmodifiable() の違い

どちらもmutate操作が行えないリストを作成するといった点で共通していますが、元になったリストに加えられた変更が反映されるかどうかといった点で違いがあります。

import 'dart:collection';

void main() {
  final source = <int>[1, 2, 3];
  final unmodifiable = List.unmodifiable(source);
  final unmodifiableListView = UnmodifiableListView(source);

  // 元のリストに変更を加える
  source.add(4);
  
  // List.unmodifiable() 
  // - 元のリストの変更は反映されない
  // - 元のリストの要素をもとに新しいインスタンスを作成し、それをmutate操作ができないようにしているため
  print(unmodifiable); // [1,2,3]
	
  // UnmodifiableListView()
  // 元のリストの変更が反映される
  // 元のリストをラップしているため、元のリストの変更が反映される
  print(unmodifiableListView); // [1,2,3,4]
}

考慮しなかったらどうなる?

mutate操作実行時にエラーを吐くが、考慮していない場合は当然このエラーに対するエラーハンドリングもしていないはずであるため、最悪アプリが落ちることが想定されます。

どうしたらいい?

既存のリストに対してmutate操作を行うのではなく、新しいListに詰め直した上でmutate操作を行うよう実装しましょう。
また、コードレビューで mutate操作が行えないリストに対してそういった操作をしていないかの確認をすると良さそうです。

Discussion