💨

DartのUnmodifiableListViewを理解する

2023/04/15に公開

https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple#changenotifier のコード例の箇所で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(できる)なので、変更できないListにしてる雰囲気があります。

動作を観察する

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

class Cart {
  final List<int> _items = [];
  
  List<int> 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 List<int> _items = [];
  
  UnmodifiableListView<int> get items => UnmodifiableListView(_items);  // getterでUnmodifiableListViewを返すようにする
  
  void addItem(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メソッドなどで要素を削除しようとするとエラーになりました。

DartのAPI Documentを読む

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

An unmodifiable List view of another List. (別のリストの、変更不可のList view)

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<E> source)
Creates an unmodifiable list backed by source. (引数sourceに基づいて変更不可のリストを作成する)

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

UnmodifiableListView(elements)とList.unmodifiable(elements)の違い

ところで、ListにはList.unmodifiable()というfactory constructorがあるらしいです。
https://api.flutter.dev/flutter/dart-core/List/List.unmodifiable.html

List<E>.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 = <int>[1, 2, 3];
final unmodifiableList = List.unmodifiable(numbers); // [1, 2, 3]
unmodifiableList[1] = 87; // 例外を投げる

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

これらの違いについて、https://stackoverflow.com/a/61404767 でちょうど解説されていたのでご紹介します。
(今更ですが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について一旦理解したことにします。

Discussion