皆さん、こんにちは!Dartの世界へようこそ!
Dartのコードを書いていると、`var`、`final`、`const`という3つのキーワードを目にする機会が多いですよね。特に`final`と`const`は「値を一度しか代入できない」という点で似ているため、「結局何が違うの?」と疑問に思った方も少なくないのではないでしょうか。
でも安心してください。ここをクリアすれば、Dartの基本はバッチリマスターできますよ。今回は、単なる代入タイミングの違いだけでなく、Dartの根幹を支える「コンパイル時評価」と「ランタイム評価」の境界線、さらにはコレクションの不変性や、Dart VMがどのようにこれらの値を扱うかといった本質まで、優しく、そして深く掘り下げていきましょう。
この記事を読み終える頃には、あなたは自信を持って`final`と`const`を使い分け、より堅牢で効率的なDartコードを書けるようになっているはずです!
—
なぜ`final`と`const`の理解が重要なのか?
「値を一度しか代入できない」って、なんだか面倒に感じるかもしれませんよね。しかし、この「不変性」という考え方は、現代のプログラミングにおいて非常に重要な役割を果たします。
1. コードの堅牢性向上: 意図しない値の変更を防ぎ、バグのリスクを減らします。
2. 可読性の向上: 「この変数は途中で変わらない」ということが明確になり、コードが読みやすくなります。
3. パフォーマンスの最適化: Dart VM(仮想マシン)やAOTコンパイラは、不変な値に対して特別な最適化を施すことができます。特に`const`は、FlutterのUIパフォーマンスに直結することも多いんですよ。
さあ、それではまずは基本から、一つずつ見ていきましょう!
—
おさらい:`var`は「なんでもあり」な変数
`var`はDartで最も一般的な変数宣言の方法です。型推論が働き、一度代入した値と同じ型のデータなら、何度でも再代入が可能です。
void main() {
var name = ‘Alice’; // String型と推論されます
print(‘名前: $name’);
name = ‘Bob’; // 再代入できます
print(‘新しい名前: $name’);
// name = 123; // エラー: ‘int’型を’String’型に代入することはできません
}
`var`は便利ですが、コードの途中で値が変わってしまう可能性があるため、常にその変数の状態を意識しておく必要があります。そこで登場するのが、不変性を保証する`final`と`const`です。
—
`final`を深掘り:ランタイムに一度だけ代入される「不変な参照」
`final`キーワードは、「一度だけ代入(初期化)できる変数」を宣言します。ポイントは、その値がランタイム(プログラムの実行時)に評価され、確定するという点です。
一度値が代入されたら、その後はその`final`変数に別の値を再代入することはできません。
`final`の基本的な使い方
void main() {
final String userName = ‘Charlie’; // 明示的に型を指定
// userName = ‘David’; // エラー: final変数’userName’には一度しか代入できません。
final currentMoment = DateTime.now(); // 実行時に現在時刻が確定
print(‘最終ユーザー名: $userName’);
print(‘現在の瞬間: $currentMoment’);
// final int randomNumber;
// randomNumber = 10; // 初期化していないfinal変数に代入
// randomNumber = 20; // 2度目の代入はできない
// もちろん、宣言時に初期化するのもOK
final int fixedNumber = 100;
print(‘固定された数値: $fixedNumber’);
// finalキーワードは、変数の「参照」が不変であることを意味します。
// つまり、この変数が指すメモリ上の場所は変わらない、ということです。
}
`DateTime.now()`のように、プログラムを実行するその瞬間まで値が分からないものは、`final`で宣言することになります。実行時に値が一度決まれば、もうその値は変更されない、と宣言しているわけですね。
コレクションと`final`:参照は不変、中身は可変の罠
ここが`final`で多くの人がつまずきやすいポイントです。`final`をコレクション(List, Mapなど)に適用した場合、そのコレクション「自体」の参照は不変になります。しかし、コレクションの「中身(要素)」は変更可能なままなんです。
イメージとしては、
final myList = [1, 2, 3];
// myList が指している「箱」は、もう別の「箱」を指すことはできない。
// でも、その「箱」の中身(要素)は自由に入れ替えたり、追加したりできる。
という感じです。
void main() {
final List
print(‘元のリスト: $numbers’); // 出力: 元のリスト: [1, 2, 3]
// numbers = [4, 5, 6]; // エラー: final変数’numbers’には一度しか代入できません。
// リストそのものの参照は変更できません。
numbers.add(4); // OK: リストの中身(要素)は変更できます。
print(‘要素を追加したリスト: $numbers’); // 出力: 要素を追加したリスト: [1, 2, 3, 4]
numbers[0] = 100; // OK: 要素の値を変更できます。
print(‘要素を更新したリスト: $numbers’); // 出力: 要素を更新したリスト: [100, 2, 3, 4]
// Dart VMの視点から見ると…
// numbersという変数自体は、ヒープメモリ上の特定のListオブジェクトのアドレスを一度だけ保持します。
// そのListオブジェクトの内部状態(要素)は、オブジェクトが可変である限り変更可能です。
}
この「参照の不変性」と「内容の不変性」の違いは、特に他の言語からDartに入ってきた方にとっては混乱しやすい点ですよね。でも、これで`final`の挙動はバッチリです!
—
`const`を深掘り:コンパイル時に確定する「真の不変な値」
さあ、いよいよ`const`の登場です!`const`キーワードは「コンパイル時に確定する、真に不変な値」を宣言します。
ここでの最大のポイントは、値がコンパイル時(プログラムを実行する前)に評価され、確定するという点です。つまり、プログラムが実行される前から、その値は完全に決まっている必要があります。
`const`の基本的な使い方
void main() {
const int maxAttempts = 3; // コンパイル時に3と確定
const String appName = ‘My Awesome App’; // コンパイル時に文字列が確定
print(‘最大試行回数: $maxAttempts’);
print(‘アプリ名: $appName’);
// maxAttempts = 5; // エラー: const変数’maxAttempts’には一度しか代入できません。
// const currentMoment = DateTime.now(); // エラー: const変数はコンパイル時に定数である必要があります。
// DateTime.now()は実行時まで値が確定しないため、constでは使えません。
}
`const`で宣言できるのは、数値リテラル、文字列リテラル、ブール値リテラル、または他の`const`変数など、コンパイル時に値を計算できるものだけです。
コレクションと`const`:参照も中身も不変な「ディープイミュータブル」
`const`をコレクションに適用した場合、`final`とは異なり、そのコレクション「自体」の参照だけでなく、コレクションの「中身(要素)」も完全に不変になります。これを「ディープイミュータブル(Deep Immutable)」と呼びます。
イメージとしては、
const myConstList = [1, 2, 3];
// myConstList が指している「箱」は、もう別の「箱」を指すことはできない。
// そして、その「箱」の中身(要素)も、もう一切変更できない。
という感じです。
void main() {
const List
print(‘定数リスト: $constNumbers’); // 出力: 定数リスト: [10, 20, 30]
// constNumbers.add(40); // エラー: List
// constNumbers[0] = 100; // エラー: List
// Dart VMの視点から見ると…
// constで宣言されたコレクションは、コンパイル時にその構造と内容が完全に固定されます。
// Dart VMは、同じ内容を持つconstコレクションが複数箇所で使われていても、
// メモリ上ではただ一つのインスタンスを共有する、という最適化(Interning / Canonicalization)を行います。
// これにより、メモリ使用量が大幅に削減され、比較も高速になります。
const List
print(identical(constNumbers, anotherConstNumbers)); // 出力: true (同じインスタンスを指している!)
// これはconstの強力な最適化の一例です。
}
`const`は、その不変性が徹底されているため、Dart VMやAOTコンパイラが最も効率的に扱えるデータ型の一つです。特にFlutterでは、`const Widget()`のようにUIの部品を`const`にすることで、不要な再構築を防ぎ、パフォーマンスを大きく向上させることができます。
`const`コンストラクタと`const`インスタンス
`const`オブジェクトを生成するためには、そのクラスのコンストラクタが`const`である必要があります。
class Point {
final int x;
final int y;
const Point(this.x, this.y); // constコンストラクタ
}
void main() {
const p1 = Point(10, 20); // constインスタンス
const p2 = Point(10, 20); // constインスタンス
print(identical(p1, p2)); // 出力: true (同じインスタンスを共有)
final p3 = Point(30, 40); // final変数だが、Pointクラスはconstコンストラクタを持つので、
// constインスタンスを代入することもできる。
// この場合はp3はconstではないので、同一インスタンス共有の最適化は行われない。
// final p4 = Point(30, 40);
// print(identical(p3, p4)); // 出力: false (別のインスタンス)
}
このように、`const`は単に変数の宣言だけでなく、オブジェクトの生成時にも深く関わってくる、Dartの重要な概念なんですよ。
—
`final`と`const`の決定的な違い:比較表でスッキリ!
これまでの話をまとめると、`final`と`const`の決定的な違いは、その「評価タイミング」と「不変性の深さ」にあります。
| 特徴 | `final` | `const` |
| :—————– | :————————————- | :————————————— |
| 評価タイミング | ランタイム(実行時)に一度だけ評価 | コンパイルタイム(実行前)に評価 |
| 代入回数 | 一度だけ | 一度だけ |
| 再代入 | 不可 | 不可 |
| 不変性の深さ | シャローイミュータブル | ディープイミュータブル |
| | (参照は不変、中身は可変) | (参照も中身も完全に不変) |
| 使用できる値 | 実行時に決まる値(`DateTime.now()`など)
またはコンパイル時に決まる値 | コンパイル時に完全に決まる値(リテラル、他の`const`など) |
| メモリ/VMの挙動| 実行時にヒープ上にオブジェクトが生成され、変数にはその参照が一度だけ格納される。 | コンパイル時に値が確定し、Dart VMは同じ値を持つ`const`インスタンスを共有(Interning)することでメモリ効率を最大化する。AOTコンパイル時にはバイナリに直接埋め込まれることも。 |
| Flutterでの役割| ウィジェットの状態など、実行時に確定するが変更されないデータ | UIツリーの不変な部分。再構築時に同一インスタンスであることを保証し、描画最適化に大きく貢献。 |
この表を見ると、違いがより明確になりますよね。
Dart VM / AOTコンパイラの視点から
私たちは普段コードを書いていますが、その裏側ではDart VMやAOTコンパイラが頑張ってくれています。
- `const`:
- AOTコンパイル時には、`const`値は完全に解決され、実行可能なバイナリに直接埋め込まれたり、アドレスが固定されたりします。これにより、実行時のルックアップが不要になり、最高のパフォーマンスと最小のメモリフットプリントを実現します。
- 同じ`const`値が複数回使われても、メモリ上ではたった一つのインスタンスが共有されるため、メモリ効率が極めて高いです。これは`Isolate`間でも安全に共有できる特性を持っています。
- `final`:
- `final`値は実行時に一度だけ計算され、その結果が格納されます。コンパイラはその後その値が変更されないことを知っているので、一部の最適化は可能ですが、`const`ほどの厳密な最適化(例えばInterning)は適用されません。
- 各Isolateは、`final`変数の初期化ロジックを独立して実行する可能性があります。
この違いを意識することで、よりパフォーマンスの高い、堅牢なアプリケーションを設計できるようになります。
—
よくある落とし穴とベストプラクティス
1. `final`コレクションの要素変更の罠
最も多い間違いがこれです。
void main() {
final List
names.add(‘Charlie’); // これはOK!finalはリストの参照をロックするだけ。
print(names); // 出力: [Alice, Bob, Charlie]
// final List
// names = otherNames; // これはNG!final変数の再代入は不可。
}
「`final`だから完全に不変だ!」と思い込んでしまうと、予期せぬ挙動につながることがあります。コレクションの中身も不変にしたい場合は、`const`を使うか、`unmodifiable`なコレクションを使う必要があります(例: `List.unmodifiable(myList)`)。
2. `const`が必要なのに`final`を使ってしまうケース
Flutterのウィジェットでよく見られます。
// Bad Practice
class MyWidget extends StatelessWidget {
final String title; // ここをconstにできる場合が多い
const MyWidget({Key? key, required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(title);
}
}
// Good Practice
class MyConstWidget extends StatelessWidget {
final String title; // このフィールドはfinalでOK
const MyConstWidget({Key? key, required this.title}) : super(key: key); // コンストラクタをconstにする
@override
Widget build(BuildContext context) {
return Text(title);
}
}
`const MyConstWidget(title: ‘Hello’)` のように、コンストラクタを`const`にすることで、そのウィジェットインスタンス自体が`const`になり、FlutterのUIツリーの再構築最適化の恩恵を最大限に受けられます。フィールドは`final`でも大丈夫ですが、コンストラクタが`const`であることは重要です。
ベストプラクティス:Dartの「できるだけ`const`」哲学
Dartでは、一般的に以下の優先順位で変数を宣言することが推奨されています。
1. `const`: 可能であれば常に`const`を使う。コンパイル時に値が確定し、不変であるため、最も効率的で堅牢です。
2. `final`: `const`が使えないが、一度初期化されたら値が変わらない場合。実行時に値が決まるが、その後の変更は許さない、という意図を明確にします。
3. `var`: どうしても後から値を変更する必要がある場合にのみ使用する。
この「できるだけ`const`」の哲学を実践することで、あなたのコードはより安定し、パフォーマンスも向上するはずです。
—
まとめ:Dartの不変性をマスターしよう!
今回は、Dartの`final`と`const`について、その決定的な違いから、Dart VMやAOTコンパイラがどのようにこれらの値を扱うかまで、深く掘り下げてきました。
- `final`: ランタイムに一度だけ代入され、その参照が不変。コレクションの中身は可変。
- `const`: コンパイル時に値が確定し、参照も中身も完全に不変。メモリ効率やパフォーマンスの最適化に大きく貢献。
この2つのキーワードを正しく理解し、使いこなすことは、Dartプログラミングにおいて非常に重要です。特にFlutter開発においては、`const`を積極的に使うことで、UIのパフォーマンスを飛躍的に向上させることができます。
これで、あなたは`var`、`final`、`const`の使い分けに自信を持てるようになったはずです。Dartの不変性の概念をマスターし、さらに一歩踏み込んだ堅牢で効率的なコードを書く旅を楽しんでくださいね!
これからも一緒にDartの奥深い世界を探求していきましょう!