DartのUnmodifiableListViewを理解する

Simple app state management | Flutterのコード例の箇所でUnmodifiableListViewというのが出てきたんですが、役割や使い所をちゃんと理解しておきたく、調べてまとめます。

ドキュメントでは以下のような例で出てきました。CartModelクラスのgetterで使われています。

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

該当箇所だけをよく見ると、

/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

un-(否定) + modify(変更する) + -able(できる)なので、変更できないリストにしてそうな雰囲気があります。

動作を観察する

動作を試してみます。まずは先ほどのコード例をdartpad.devでも動くように単純にした上で、UnmodifiableListViewを使わないようにしました。

class Cart {
  final _items = [];
  
  get items => _items; // _items配列をそのまま返すgetter
  
  void addItem(int item) {
    _items.add(item);
  }
}

main() {
  final cart = Cart();
  print(cart.items); // => []

  cart.addItem(3);  // CartクラスのaddItemメソッドを使えば...
  print(cart.items); // => [3] // ...もちろん_items配列に追加されて、items getterで取り出せる

  final list = cart.items; // items getterの戻り値はList<int>なので...
  list.add(100);  // ...List型に備わっているaddメソッドが使えてしまい...
  print(list); // => [3, 100] // ... listに追加できるし...
  print(cart.items); // => [3, 100] // ...Cartクラスの_items配列自体も変わっている
}

一方、UnmodifiableListViewを使うとこのようになります。

import 'dart:collection';  // UnmodifiableListViewを使うために必要

class Cart {
  final _items = [];
  
  get items => UnmodifiableListView(_items);  // getterでUnmodifiableListViewを返すようにする
  
  void add(int item) {
    _items.add(item);
  }
}

main() {
  final cart = Cart();
  print(cart.items); // => []

  cart.addItem(3);
  print(cart.items); // => [3]. // addItemメソッドの挙動は変わらないが...
  
  final unmodifiableList = cart.items;  // items getterの戻り値がUnmodifiableListView、つまり変更不可のListになり...
  unmodifiableList.add(100); // => ...追加しようとした時点でエラーが出る! Uncaught Error: Unsupported operation: Cannot add to an unmodifiable list (変更不可のListには追加できません)
}

だいぶ分かりやすい違いがありました。items getterの返り値は文字通り変更不可のリストであるため、addメソッドで要素を追加したり、removeAtメソッドなどで要素を削除しようとするとエラーになりました。

DartAPI Documentを読む

動作を見た段階で何となく理解した気はしますが、一応DartのドキュメントのUnmodifiableListView class - dart:collection library - Dart APIを読んでおきます。 以下のように書いてあります。

An unmodifiable List view of another List. (他のリストの、変更不可のListのラッパー)

The source of the elements may be a List or any Iterable with efficient Iterable.length and Iterable.elementAt. (要素のsourceは、Listまたは効率的な Iterable.length と Iterable.elementAt を持つ任意の Iterableかもしれない)

Constructors UnmodifiableListView(Iterable source) Creates an unmodifiable list backed by source. (sourceというIterableに裏付けられた変更不可のリストを作成する)

UnmodifiableListViewのコンストラクタの引数は、 Iterable.lengthとIterable.elementAtを持つIterableですが、基本的にはListだと思っておいて良さそうですね。

ところで、ListにはList.unmodifiable()というfactory constructorがあるらしいです。 List.unmodifiable constructor - List - dart:core library - Dart API

List.unmodifiable(Iterable elements) Creates an unmodifiable list containing all elements. (すべての要素を含む変更不可のリストを作成する)

The Iterator of elements provides the order of the elements. (要素のIteratorは、要素の順序を提供する)

An unmodifiable list cannot have its length or elements changed. If the elements are themselves immutable, then the resulting list is also immutable. (変更不可のリストは、その長さや要素を変更することができない。 要素自体が不変であれば、結果として得られるリストも不変である)

final numbers = [1, 2, 3]; final unmodifiableList = List.unmodifiable(numbers); // [1, 2, 3] unmodifiableList[1] = 87; // 例外を投げる

UnmodifiableListView(elements)とList.unmodifiable(elements)、どちらも引数のelementsというListを変更不可にしたものを返すという意味では大体同じに見えます。

これらの違いについて、In Dart, what different about List.unmodifiable() and UnmodifiableListView? - Stack Overflowでちょうど解説されていたのでご紹介します。 (今更ですがList viewの「view」って、MVCなどの画面表示とかのviewじゃなくて、wrapperという意味合いのようですね)

この解答を読むと、変更不可Listを作ったあとに元のListを変更したときの挙動に違いがありそうです。

UnmodifiableListView(elements)のほうは、元のListの変更が変更不可Listにも波及します。つまり、変更不可Listは直接変更できませんが、元のListを変更することで間接的に変更することができます。使い所としては、最初に引用した例の_itemsのように、クラスのプライベートフィールドを元にUnmodifiableListViewを作成し、クラス内部のメソッドからは変更を許すが、クラス外部からの変更はできないようにするという場面かと思います。これによってカプセル化できるということになります。

一方でList.unmodifiable(elements)のほうは、元のListとは全く別の新しい変更不可Listを作ります。変更不可Listを作ったあとに元のListを変更しても、変更不可Listのほうには何の影響もありません。使い所としては、List.unmodifiableで変更不可Listを動的に作ったら、それ以後は一切変更しない定数のような扱いをするような場面かなと思います。

以上でUnmodifiableListViewについて一旦理解したことにします!参考になれば幸いです。