状態管理の隠蔽と「意図しない変異」の排除
コードレビューをしていて、最も背筋が凍る瞬間のひとつは、ドメイン層やデータ層が保持する内部状態(State)のコレクションが、そのままUI層や外部APIへむき出しで渡されているコードを見たときだ。
// 良くあるアンチパターン
class UserSession {
final List
// 危険:内部の参照をそのまま返している
List
}
// 呼び出し側
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
final List
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 =
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
final Map
/// 1. パフォーマンス重視:読み取り専用ビュー(List)を公開
/// 内部の _roles が変化しないことが保証されたシステム境界の内側で使う
List
/// 2. セキュリティ・整合性重視:完全にイミュータブルなスナップショットを公開
/// 外部APIやプラグインなど、何をするか分からないレイヤーへ渡す場合
List
/// 3. Mapの保護:UnmodifiableMapBase または Map.unmodifiable
Map
// 内部状態を安全に変更するためのメソッド(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コードの信頼性は一段と洗練されたものになるはずだ。次のコードレビューでは、不用意にリストを返しているゲッターを見つけたら、即座にこれらを適用させよう。