【実務・中級編】Dartのコレクションにおける「unmodifiable」なビューの活用と防御的コピー – Dart コア文法・オブジェクト指向・Null安全解析バイブル

状態管理の隠蔽と「意図しない変異」の排除

コードレビューをしていて、最も背筋が凍る瞬間のひとつは、ドメイン層やデータ層が保持する内部状態(State)のコレクションが、そのままUI層や外部APIへむき出しで渡されているコードを見たときだ。

// 良くあるアンチパターン
class UserSession {
final List _permissions = [‘read’, ‘write’];

// 危険:内部の参照をそのまま返している
List get permissions => _permissions;
}

// 呼び出し側
void main() {
final session = UserSession();
session.permissions.add(‘admin’); // 簡単に内部状態が破壊される
}

このコードの問題は、`_permissions` というプライベート変数にしているにもかかわらず、ゲッター経由でミュータブルな参照そのものがリークしている点にある。Dartのオブジェクトはすべて参照渡しだ。これではカプセル化が完全に崩壊し、バグの温床となる。

今回は、Dartのコレクションにおける `unmodifiable` なビューの仕組みと、パフォーマンスを極限まで考慮した防御的コピー(Defensive Copy)の使い分けについて、Dart VMの内部挙動を踏まえながらロジカルに解説しよう。

—

1. `UnmodifiableListView` とは何者か?

Dartの `collection` パッケージ(または `core`)が提供する `UnmodifiableListView` などのラッパーは、新しい配列の複製を作らずに、既存のListへの「書き込み操作をコンパイル時・実行時で阻止する薄いビュー(View)」を提供する。

VMの視点:なぜメモリ効率が良いのか?

通常の `List.from()` などを用いた防御的コピーは、O(N)の計算量でヒープ上に新しい配列オブジェクトをアロケーションし、全要素の参照をコピーする。メモリプレッシャーが高く、特に頻繁にポーリングされる状態管理ストアなどではGC(ガベージコレクション)のスパイクを引き起こす主原因になる。

一方、`UnmodifiableListView` は次のような構造をしている:

// 概念的な内部実装イメージ
class UnmodifiableListView extends ListBase {
final List _source;
UnmodifiableListView(this._source);

@TaintAsImmutable // 読み取り操作のみ委譲
@override
E operator [](int index) => _source[index];

@override
int get length => _source.length;

// 書き込み系メソッドはすべて例外をスローする
@override
void operator []=(int index, E value) => throw UnsupportedError(‘Cannot modify an unmodifiable list’);

@override
void add(E element) => throw UnsupportedError(…);
}

元となる `_source` リストへの参照を1つ保持しているだけなので、アロケーションコストは最小限(O(1))だ。ただし、注意しなければならないのは、これはあくまで「ビュー」であるという点だ。

—

2. ビュー(View)の罠:元データが変異する恐怖

ここがテクニカルリードとして最も強く警告しておきたいポイントだ。`UnmodifiableListView` は「ラッパー側からの書き込み」を防ぐが、元データ(ソース)側が書き換わった場合、ビュー側から見える中身も連動して変わる。

void main() {
final mutableList = [‘apple’, ‘banana’];
final readOnlyView = UnmodifiableListView(mutableList);

print(readOnlyView); // [apple, banana]

// 元データを外部から(あるいは別スレッドや別メソッドから)変更する
mutableList.add(‘cherry’);

// ビュー側も変更されてしまう!
print(readOnlyView); // [apple, banana, cherry]
}

これが「ビュー」の本質だ。もし「元データが将来的にどう変化しようとも、渡した時点のスナップショットを完全に保護したい」のであれば、`UnmodifiableListView` ではなく、別の戦略が必要になる。

—

3. 実務における最適解:使い分けの指針

では、現場のコンポーネント設計においてどのようにこれらを使い分べきか。判断基準は明確だ。

| 手法 | アロケーションコスト | カプセル化の強度 | スナップショット性 | 主なユースケース |
| :— | :— | :— | :— | :— |
| 生データ返却 (アンチパターン) | O(1) | ❌ ゼロ | なし | 絶対に避けるべき |
| UnmodifiableListView | O(1) (極小) | ⭕ 高い | なし(連動する) | 内部状態のリードオンリー公開(パフォーマンス重視) |
| List.unmodifiable() | O(N) | ⭕ 非常に高い | あり(複製を生成) | 完全なイミュータブル性の保証、初期化時データ |

—

4. プロダクションコード例:堅牢なアーキテクチャ設計

FlutterのBLoCやRiverpod、あるいはクリーンアーキテクチャのユースケース層を想定した、実務でそのまま使える堅牢なストアクラスの実装例を示す。

import ‘dart:collection’;

/// ユーザーの権限と設定を管理するセキュアなストア
class UserSecureStore {
// 内部のミュータブルな状態
final List _roles = [‘user’];
final Map _metadata = {‘loginCount’: 1};

/// 1. パフォーマンス重視:読み取り専用ビュー(List)を公開
/// 内部の _roles が変化しないことが保証されたシステム境界の内側で使う
List get roles => UnmodifiableListView(_roles);

/// 2. セキュリティ・整合性重視:完全にイミュータブルなスナップショットを公開
/// 外部APIやプラグインなど、何をするか分からないレイヤーへ渡す場合
List get rolesSnapshot => List.unmodifiable(_roles);

/// 3. Mapの保護:UnmodifiableMapBase または Map.unmodifiable
Map get metadata => UnmodifiableMapBase.from(_metadata);

// 内部状態を安全に変更するためのメソッド(Mutation API)
void addRole(String newRole) {
if (_roles.contains(newRole)) return;

// 必要であればここでバリデーションやドメインルールを適用
_roles.add(newRole);
}
}

void main() {
final store = UserSecureStore();

// 読み取りは正常に行える
print(store.roles); // [user]

try {
// ❌ コンパイルは通るが、実行時に UnsupportedError がスローされる
store.roles.add(‘admin’);
} catch (e) {
print(‘Caught expected error: $e’);
}

// 内部の正式なメソッドを経由した安全な変異
store.addRole(‘moderator’);
print(store.roles); // [user, moderator]
}

—

チーフアーキテクトからの総括

Dartにおいて、「ただ動くコード」を書くことは難しくない。しかし、「大規模化しても破綻しない、予測可能性の高いコード」を書くには、データがメモリ上でどう振る舞うか(参照と実体の関係)を常に意識する必要がある。

  • 外部へコレクションを公開するときは、必ず何らかの防御策を講じること。
  • 呼び出し元で書き換えられては困るが、パフォーマンスを極限まで高めたい場合は `UnmodifiableListView`。
  • データの完全なスナップショットを保証したい、あるいは外部境界を跨ぐ場合は `List.unmodifiable()` による防御的コピー。

この2つを文脈に合わせて的確に選択できるようになれば、あなたの書くDartコードの信頼性は一段と洗練されたものになるはずだ。次のコードレビューでは、不用意にリストを返しているゲッターを見つけたら、即座にこれらを適用させよう。

タイトルとURLをコピーしました