こんにちは!Dartの世界へようこそ。
Flutterを使ったアプリ開発や、Dart単体でのバックエンド開発、楽しんでいますか?
他のプログラミング言語(例えばJavaやC#、JavaScriptなど)からDartの世界に入ってきたとき、ふとこんな疑問を持ったことはありませんか?
「あれ? Dartのクラスって、なんで `getter` はあるのに、変更用の `setter` を安易に作っちゃいけないんだろう?」
「`final` ってつけておけば安全って聞くけど、内部の挙動はどうなっているの?」
今回は、Dartのオブジェクト指向の根幹をなす `final` 変数と「不変性(Immutability)」を強制するクラス設計の基本 について、少しだけ深掘りしてお話ししていきますね。
ここをしっかりとクリアできれば、あなたの書くコードの堅牢性は劇的に跳ね上がります。一緒にバッチリマスターしていきましょう!
—
1. なぜ「変更できないこと(不変性)」がそんなに重要なのか?
アプリが大きくなったり、複数人での開発チームで作業したりするようになると、こんなバグに頭を悩まされた経験はありませんか?
> 「あれ? 画面Aで持っていたユーザーデータの名前が、いつの間にか画面Bの処理で書き換わっちゃってる……! どこで誰が書き換えたんだ!?」
変数をいつでも、どこからでも自由に変更できるようにしている(これを「可変=Mutable」と呼びます)と、プログラムの状態変化を追うのが一気に難しくなります。
そこでDartでは、「オブジェクトを作ったあとに、その中身を勝手に書き換えられないようにする(=不変=Immutable)」 という設計を非常に強く推奨しています。そのための最大の武器が、今回主役となる `final` キーワードです。
—
2. `final` フィールドとコンストラクタの基本形
まずは、外部から中身を変更されない、堅牢なクラスの基本形を見てみましょう。
class User {
// すべてのフィールドに final を付与し、インスタンス生成後の書き換えを禁止する
final String id;
final String name;
final int age;
// コンストラクタで初期値を注入する(一度入れたら、もう二度と変更できない)
const User({
required this.id,
required this.name,
required this.age,
});
}
void main() {
// インスタンスの生成
const user = User(id: ‘A-001’, name: ‘Alice’, age: 25);
print(‘ユーザー名: ${user.name}’); // 出力: ユーザー名: Alice
// 【注目】ここで値を書き換えようとすると…?
// user.name = ‘Bob’; // 🛑 コンパイルエラー!
}
コードの裏側で何が起きているか?
`final` がついた変数は、「一度だけ値を代入でき、その後は二度と再代入(変更)できない」 という制約をDartのコンパイラに強制します。
もし誰かがうっかり `user.name = ‘Bob’` なんてコードを書こうものなら、Dartは実行するまでもなく、コンパイルの瞬間にエラーを出して止めてくれます。これが、バグを未然に防ぐ最高のセーフティネットになるんです。
—
3. 「setter」を安易に作らないという思想
他のオブジェクト指向言語では、フィールドを `private` に隠蔽して、値を書き換えるための `setXxx()` やセッターメソッドを生やすのが「お作法」とされていることも多いですよね。
ですが、Dartのモダンな設計思想において、不必要なセッターを作ることは「安全な状態を自ら壊す行為」 とみなされます。
もしセッターを作ってしまうと、以下のような状態が生まれます。
// ⚠️ 避けるべき設計の例(セッターを持たせた可変クラス)
class BadUser {
String name; // finalではない
BadUser(this.name);
// 値を書き換えるためのセッター(あるいはメソッド)
set name(String newName) {
if (newName.isNotEmpty) {
_name = newName; // 内部で書き換え可能になってしまう
}
}
}
これをしてしまうと、「この `BadUser` インスタンスは、アプリのどこかの処理で名前が変わっているかもしれない」という不安を常に抱えながらコードを読むことになります。
「じゃあ、値を変更したいときはどうするの?」
「名前が変わったユーザー」を扱いたいときは、元のオブジェクトの値を書き換えるのではなく、「新しい値を持った、別の新しいオブジェクトを新しく生み出す」 というアプローチを取ります。
Dartのクラスには、これをエレガントに行うための `copyWith` というイディオム(慣用句)がよく使われます。
class ImmutableUser {
final String id;
final String name;
final int age;
const ImmutableUser({
required this.id,
required this.name,
required this.age,
});
// 値の一部を変更した「新しいインスタンス」を返すメソッド
ImmutableUser copyWith({
String? name,
int? age,
}) {
return ImmutableUser(
id: this.id, // idは変えない
name: name ?? this.name, // 引数が渡されなければ、元の値を維持
age: age ?? this.age,
);
}
}
void main() {
var user1 = const ImmutableUser(id: ‘001’, name: ‘Charlie’, age: 30);
print(user1.name); // Charlie
// 名前だけを変えた「新しいユーザー」を作る!
var user2 = user1.copyWith(name: ‘David’);
print(user1.name); // Charlie (元のuser1は一切汚染されていない!)
print(user2.name); // David (新しく生まれたuser2)
}
この「データを破壊せず、常に新しいデータを返す(Immutable / Functional なアプローチ)」は、FlutterのUI状態管理(ProviderやRiverbloc、Blocなど)とも非常に相性が良く、画面の再描画バグを防ぐための強力な基盤となります。
—
4. 初学者が陥りがち! `final` と `const` の違い
ここで、よくある混乱ポイントをスッキリ整理しておきましょう。似ているようで、Dartにおけるこの2つは役割が全く違います。
- `final`: 「実行時」に一度だけ値を代入できる(代入後は変更不可)。
- 例:現在の時刻を入れる、ユーザーからの入力値をセットするなど、プログラムが動いてから決まる値に使う。
- const: 「コンパイル時(コードを書いている時点)」に完全に値が確定している。
- 例:数学の円周率 `3.14159` や、固定の文字列など。
void main() {
final now = DateTime.now(); // OK: 実行時の時刻が入る
// const today = DateTime.now(); // 🛑 コンパイルエラー! 「今何時かなんて、コードを書く時には分からないよ!」とDartに怒られます。
}
クラスのフィールドに `final` を使うときは、「このオブジェクトが存在している間、この値は絶対にブレない」 という約束をコンパイラと交わしているとイメージしてください。
—
まとめ:堅牢なクラス設計のチェックリスト
ここまでの内容をギュッとまとめます。明日からのコーディングで、ぜひ以下のポイントを意識してみてくださいね。
1. クラスのフィールドは、基本すべて `final` にできないか検討する。
2. 外部から書き換えさせるための「セッター」や「変更メソッド」を安易に作らない。
3. 値を変更したいときは、破壊的な変更ではなく `copyWith` パターンを使って新しいインスタンスを生成する。
4. `final`(実行時の不変)と `const`(コンパイル時の完全な定数)を適切に使い分ける。
この基本をマスターするだけで、あなたの書くDartコードの信頼性は一段も二段もレベルアップします。「意図しないデータの書き換え」という魔物から解放された、スッキリとしたコードが書けるようになりますよ。
それでは、次回の記事でもDartの深い魅力と知見を一緒にお届けしていきます。お楽しみに!