こんにちは!Dartの奥深い世界へようこそ。FlutterやDartを使った開発に熱中していると、「あれ? このリスト、勝手に書き換えられて困るんだけど……」という場面に直面したことはありませんか?
他の言語からやってきた方だと、「読み取り専用(Read-only)にするにはどう書けばいいんだっけ?」と手が止まることも多いはずです。
今回は、Dartのコレクションにおける「unmodifiable(変更不可)なビュー」と「防御的コピー」の本質について、裏側の仕組みまでしっかりと紐解いていきますね。ここをクリアすれば、データ構造の安全性を完全にコントロールできるようになりますよ。それでは、一緒に見ていきましょう!
—
1. なぜコレクションの「意図しない書き換え」が怖いのか?
まずは、よくある危険なコードからお見せしますね。
class UserSession {
// 内部で保持している機密データ(のつもり)
final List
List
}
void main() {
final session = UserSession();
// 外部からアクセスして……
final userPerms = session.permissions;
// えっ、勝手に追加できちゃった!?
userPerms.add(‘delete’);
print(session.permissions); // 出力: [read, write, delete]
}
「あれ? `_permissions` は `final` で宣言したのに、中身が書き換わってる!?」と驚きませんでしたか?
ここがDart(そして多くのモダン言語)の罠になりやすいポイントです。`final` は「変数に別のオブジェクトを再代入できない」ことを保証するだけで、オブジェクトそのものの中身(ミュータブルな状態)まで凍結するわけではありません。
クラスの内部で作ったリストをそのまま `getter` で返してしまうと、呼び出し元がどこからでもリストをいじり放題になってしまいます。これではカプセル化(データの隠蔽)が台無しですね。
—
2. 救世主登場:`UnmodifiableListView` とは何か?
この問題をスマートに解決するのが、`dart:collection` ライブラリに含まれる `UnmodifiableListView`(および `UnmodifiableMapView` など)です。
これらは、元のリストを「包み込む(ラップする)」ことで、外側からデータの追加・削除・変更を一切できなくする「読み取り専用の窓(ビュー)」を作り出します。
実際のコードで使い方を確認してみましょう。
import ‘dart:collection’;
class SecureUserSession {
final List
// UnmodifiableListViewでラップして公開する
UnmodifiableListView
}
void main() {
final session = SecureUserSession();
final userPerms = session.permissions;
// 読み取りは普通にできる
print(userPerms[0]); // 出力: read
try {
// 書き込もうとすると……?
userPerms.add(‘delete’);
} catch (e) {
print(‘エラー発生: $e’);
// 出力: エラー発生: Unsupported operation: Cannot add to an unmodifiable list
}
}
ビュー(View)の本質:コピーではなく「監視つきの窓」
ここで重要なポイントがあります。`UnmodifiableListView` は、元のリストのデータを複製(コピー)しているわけではありません。
イメージ図で表すと、このような構造になっています。
[ 内部データ ] <--- (元のList) ['read', 'write'] ↑ │ ラップしている(実体は同じメモリ上のリスト) │ [ 読み取り専用窓 ] <-- (UnmodifiableListView) ※ここからの変更操作は例外でブロックされる つまり、もしクラスの内部側(`SecureUserSession`)でデータを追加・変更した場合、その変化は外部の `permissions` ビューからもリアルタイムで見えるようになっています。
「読み取り専用にしたいけれど、内部での更新は外部に反映させたい」というケースにおいて、メモリ効率を落とさずに安全性を確保できる最高の選択肢になります。
—
3. もう一つのアプローチ:「防御的コピー(Defensive Copy)」
「ビュー」ではなく、完全に独立したコピーを渡したい場面もありますよね。そんなときに使うのが 防御적コピー です。
Dartでは、リストのコンストラクタ(`.from` や `.of`)を使って新しいリストを生成し、それを返します。
class ClonedUserSession {
final List
// 完全に切り離された新しいリストを返す
List
}
void main() {
final session = ClonedUserSession();
final userPerms = session.permissions;
// 外部で変更しても、内部の _permissions には影響しない!
userPerms.add(‘delete’);
print(session.permissions); // 出力: [read, write] (元のデータは守られた)
}
`UnmodifiableListView` と「防御的コピー」の使い分け
どちらを使うべきか、現場で迷ったときの判断基準をまとめておきますね。
| 比較項目 | `UnmodifiableListView` (ビュー) | 防御的コピー (`List.from` など) |
| :— | :— | :— |
| メモリ効率 | 高(既存リストを共有するため軽量) | 低(要素数に比例して新規メモリを消費) |
| データの連動 | 内部の変更が反映される | 完全に切り離され、連動しない |
| 安全性 | 変更操作は実行時エラーになる | 変更しても他に影響しない(安全) |
| おすすめの場面 | 頻繁にアクセスされ、内部の最新状態を安全に見せたいとき | 外部にデータを「プレゼント」し、内部の改ざんを完全に防ぎたいとき |
数千件以上の要素を持つ大きなリストを何度もコピーすると、ガベージコレクション(GC)の負荷になりパフォーマンス低下を招きます。そういった文脈では、`UnmodifiableListView` のようなビューの活用が圧倒的に有利です。
—
4. 陥りやすい文法エラーと注意点
初心者の頃によやりがちなミスをいくつかピックアップしておきます。コンパイル時や実行時でつまずかないように頭の片隅に入れておきましょう。
① `const` リストと混同しない
`const [1, 2, 3]` は「コンパイル時定数」です。これは最初から完全に変更不可能なコレクションになりますが、実行時に動的にデータを生成・変更するクラスの内部状態には使えません。動的なリストを保護したいときは `UnmodifiableListView` の出番です。
② `UnmodifiableListView` は型指定に注意する
インポートし忘れてエラーになるケースがよくあります。必ずファイルの一番上で以下をインポートしてください。
import ‘dart:collection’;
—
まとめ:ここをクリアすればDartの基本はバッチリ!
今回は、Dartのコレクションにおける「安全性の担保」について解説しました。
- `final` だけではリストの中身の変更までは防げない。
- `UnmodifiableListView` を使えば、メモリを無駄にコピーせずに「読み取り専用の窓」を提供できる。
- 完全にデータを切り離したい場合は 防御的コピー(`List.from`) を選択する。
この仕組みを理解できると、単に動くコードを書くだけでなく、「堅牢で予測しやすい設計」ができるようになり、ワンランク上のエンジニアへとグッと近づきます。
オブジェクト指向のルールを守りながら、安全でパフォーマンスの高いDartコードを書いていきましょう!それではまた次回の記事でお会いしましょう。