Dart 変数とデータ型 — 5 つの宣言方法
変数はデータの入れ物、型は入れ物の形である — 適切な入れ物を選べば、コードは安全になる。
1. 学べること
- 5 つの宣言方法の比較:var / final / const / late / dynamic
- 組み込み型:int / double / String / bool / num
- 型推論と明示的アノテーションのベストプラクティス
- 文字列補間と複数行文字列(トリプルクォート + r プレフィックス)
- Bob のシナリオ:DataPipeline での設定変数の設計
2. 開発者のリアルな物語
(1) 課題:場当たり的な変数宣言がランタイムクラッシュを招く
Bob が DataPipeline の初期バージョンを開発していたとき、彼はすべての変数を var で宣言し、途中で型をよく変更した。リファクタリング中に誤って int orderCount を String orderCount に変更してしまった。コンパイルはエラーを報告せず(動的型)、実行時に orderCount * price が例外を投げ、50 万件のバッチ処理が中断され、レポートの納品が 4 時間遅れた。
(2) 型安全な解決法
Dart は緩いものから厳しいものまでさまざまな宣言方法を提供する:const(コンパイル時定数)→ final(実行時定数)→ var(推論型)→ late(遅延初期化)→ dynamic(動的型)。これらを適切に使い分ければ、コンパイラがバグの発見を助けてくれる。
// DataPipeline 設定のベストプラクティス
const int maxRecords = 1000000; // コンパイル時定数
final String pipelineName; // 実行時定数(1 回だけ設定)
var processedCount = 0; // int として推論、ミュータブル
late String outputPath; // 後で初期化
// dynamic は可能な限り避ける
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- コンパイラがコンパイル時に型エラーを検出し、ランタイムクラッシュを防ぐ
const変数はコンパイラによるパフォーマンス最適化を可能にするfinalは偶発的な変更を防ぎ、コードの意図がより明確になる
3. 5 つの宣言方法
(1) 宣言方法決定ツリー
graph LR
subgraph 宣言
A[var] --> B[型推論]
C[final] --> D[実行時定数]
E[const] --> F[コンパイル時定数]
G[late] --> H[遅延初期化]
I[dynamic] --> J[動的型]
end
subgraph 組み込み型
K[int] --- L[double]
M[String] --- N[bool]
end
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:var — 型推論
void main() {
var name = 'DataPipeline'; // String として推論
var count = 1000; // int として推論
var rate = 0.08; // double として推論
// 同じ型で再代入可能
name = 'Analytics'; // OK - String のまま
count = 2000; // OK - int のまま
// 型は変更できない
// count = 'two thousand'; // コンパイルエラー!
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:final — 実行時定数
void main() {
// 実行時に 1 回だけ設定
final DateTime startTime = DateTime.now();
final String configPath = '/etc/datapipeline/config.yaml';
// 再代入不可
// configPath = '/other/path'; // コンパイルエラー!
// コレクションの final - 中身は変更可能
final List<String> sources = ['orders.csv', 'products.csv'];
sources.add('customers.csv'); // OK - 中身の変更
// sources = ['new_list']; // エラー - 参照の再代入不可
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:const — コンパイル時定数
void main() {
// コンパイル時に判明している必要がある
const int maxRecords = 1000000;
const double taxRate = 0.08;
const String version = '1.0.0';
// const コレクションは深くイミュータブル
const List<String> formats = ['json', 'csv', 'html'];
// formats.add('xml'); // エラー!const リストは変更不可
// const コンストラクタ
const config = PipelineConfig(batchSize: 10000);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:late — 遅延初期化
class DataPipeline {
late String outputPath; // 後で設定
void configure(String path) {
outputPath = path;
}
late final int maxRecords = _loadMaxRecords(); // 初回アクセス時に計算
int _loadMaxRecords() {
print('Loading max records config...');
return 1000000;
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:dynamic — 動的型
void main() {
dynamic value = 'hello'; // String
value = 42; // OK - int になる
value = [1, 2, 3]; // OK - List になる
// コンパイル時の型チェックなし - 危険!
// value.nonExistentMethod(); // コンパイルエラーなし、ランタイムクラッシュ
// 柔軟性が必要なときは dynamic より Object? を推奨
Object? safeValue = 'test';
// safeValue.nonExistentMethod(); // コンパイルエラー - より安全
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) 5 つの宣言方法の比較
| 宣言方法 | 型 | ミュータブル | 初期化タイミング | 安全性 | 用途 |
|---|---|---|---|---|---|
const |
コンパイル時決定 | イミュータブル | 宣言時 | 最高 | 設定定数、列挙値 |
final |
実行時決定 | イミュータブル | 宣言時またはコンストラクタ内 | 高 | ランタイム設定、注入値 |
var |
推論 | ミュータブル | 宣言時 | 中 | ローカル変数、カウンター |
late |
明示的アノテーション | ミュータブル | 遅延 | 中 | 遅延初期化フィールド |
dynamic |
実行時 | ミュータブル | いつでも | 低 | JSON パース、相互運用 |
4. 組み込み型の詳細
(1) 数値型
▶ サンプル:int と double
void main() {
int orderCount = 1500;
double totalAmount = 52500.75;
num genericNum = 42; // num は int と double のスーパークラス
genericNum = 3.14; // OK - num は両方を受け入れる
// 算術演算
double avgOrderValue = totalAmount / orderCount;
int roundedDown = totalAmount.toInt();
// 便利なメソッド
print(totalAmount.toStringAsFixed(2)); // 52500.75
print(orderCount.isEven); // false (1500 は偶数、true)
print(orderCount.clamp(0, 1000)); // 1000 (最大値にクランプ)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 型 | 範囲 | 用途 |
|---|---|---|
int |
64 ビット(JS では異なる) | カウント、インデックス |
double |
64 ビット IEEE 754 | 金額、比率 |
num |
int + double のスーパークラス | 一般的な数値 |
(2) String 型
▶ サンプル:文字列補間と操作
void main() {
// 文字列補間
String product = 'DataPipeline';
int users = 5000;
print('$product has $users users'); // 単純な補間
print('Revenue: ${users * 99} USD'); // 式の補間
print('Average: ${(125000 / users).toStringAsFixed(2)} USD');
// 複数行文字列
String description = '''
DataPipeline - E-Commerce Analytics Tool
Processes up to 1,000,000 orders per batch
Supports JSON, CSV, and HTML output formats
''';
// ロー文字列(エスケープ処理なし)
String path = r'C:\Users\Bob\data\orders.csv';
String regex = r'\d{4}-\d{2}-\d{2}'; // 日付パターン
// 便利なメソッド
String raw = ' Hello, World! ';
print(raw.trim()); // 'Hello, World!'
print(raw.toUpperCase()); // ' HELLO, WORLD! '
print('USD 1,500.00'.replaceAll(',', '')); // 'USD 1500.00'
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 機能 | 構文 | 例 |
|---|---|---|
| 単純な補間 | $variable |
'$name' |
| 式の補間 | ${expr} |
'${a + b}' |
| 複数行文字列 | '''...''' |
トリプルクォート |
| ロー文字列 | r'...' |
r'\n' はそのまま |
(3) bool 型
▶ サンプル:ブール値と条件
void main() {
bool isProduction = true;
bool isDebug = false;
// Dart は条件に明示的な bool を要求する
int count = 0;
// if (count) {} // エラー!bool 必須
if (count > 0) { // OK - 明示的な比較
print('Has records');
}
// 論理演算子
bool shouldProcess = isProduction && !isDebug;
bool hasData = count > 0 || isProduction;
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
5. 型推論と明示的アノテーション
(1) ベストプラクティス
// 良い - ローカル変数はコンパイラに推論させる
var name = 'DataPipeline'; // String と推論
var count = 100; // int と推論
var items = <String>[]; // 明示的なジェネリック、List と推論
// 良い - 公開 API は明示的に型付け
String get displayName => name;
int get maxCapacity => 1000000;
// 良い - 変更されない値には final を使う
final startTime = DateTime.now();
// 避ける - ローカル変数に不要な明示型
// String name = 'DataPipeline'; // 冗長
// int count = 100; // 冗長
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| シナリオ | 推奨 | 理由 |
|---|---|---|
| ローカル変数 | var / final |
冗長性を削減、推論で十分 |
| 公開 API | 明示的アノテーション | 明確なドキュメント、安定したインターフェース |
| コレクションジェネリック | 明示的ジェネリック | List<dynamic> として推論されるのを避ける |
| コンストラクタパラメータ | 明示的アノテーション | 明確な契約 |
6. Bob のシナリオ:DataPipeline の設定変数
▶ サンプル:設定変数の設計
// DataPipeline 設定定数
const String appName = 'DataPipeline';
const int defaultBatchSize = 10000;
const double defaultTaxRate = 0.08;
// ランタイム設定
class PipelineConfig {
final String version;
final int batchSize;
final String outputFormat;
PipelineConfig({
this.version = '1.0.0',
this.batchSize = defaultBatchSize,
this.outputFormat = 'json',
});
@override
String toString() =>
'$appName v$version | Batch: $batchSize | Format: $outputFormat';
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
7. 完全なサンプル:DataPipeline 変数宣言の実践
// ============================================
// DataPipeline 変数宣言デモ
// すべての宣言型の正しい使い方を示す
// ============================================
// コンパイル時定数 - 実行間で決して変わらない
const String appName = 'DataPipeline';
const int maxRecords = 1000000;
const double taxRate = 0.08;
const List<String> supportedFormats = ['json', 'csv', 'html'];
class Order {
final String id; // 構築後にランタイム定数
final double amount; // 構築後にランタイム定数
late String status; // 処理ロジックで設定
Order({required this.id, required this.amount});
double get tax => amount * taxRate;
double get total => amount + tax;
String get formattedAmount =>
'\$${amount.toStringAsFixed(2)} USD';
}
class DataPipeline {
final String name;
var processedCount = 0; // ミュータブルカウンター
late final DateTime startTime; // 初回実行時に設定
DataPipeline({required this.name});
void start() {
startTime = DateTime.now();
print('$appName started at $startTime');
}
void processOrder(Order order) {
processedCount++;
order.status = 'processed';
print('Order ${order.id}: ${order.formattedAmount} '
'(tax: \$${order.tax.toStringAsFixed(2)} USD)');
}
void printSummary() {
print('\n=== Summary ===');
print('Pipeline: $name');
print('Processed: $processedCount orders');
print('Capacity: $processedCount / $maxRecords');
}
}
void main() {
final pipeline = DataPipeline(name: 'E-Commerce Analytics');
pipeline.start();
final orders = [
Order(id: 'ORD-001', amount: 1500.00),
Order(id: 'ORD-002', amount: 3250.50),
Order(id: 'ORD-003', amount: 890.25),
];
for (final order in orders) {
pipeline.processOrder(order);
}
pipeline.printSummary();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
DataPipeline started at 2024-01-15 10:30:00.000
Order ORD-001: $1500.00 USD (tax: $120.00 USD)
Order ORD-002: $3250.50 USD (tax: $260.04 USD)
Order ORD-003: $890.25 USD (tax: $71.22 USD)
=== Summary ===
Pipeline: E-Commerce Analytics
Processed: 3 orders
Capacity: 3 / 1000000
❓ よくある質問
Q: var と final のどちらを使うべきですか? A: 再代入しない変数は
finalを優先してください。変更が必要な場合はvarを使います。公式 Dart lint ルールのprefer_final_localsもfinalの優先を推奨しています。
Q: const と final の違いは何ですか? A:
constはコンパイル時定数で、値はコンパイル時に決定されている必要があります。finalは実行時定数で、1 回だけ代入できますが、値は実行時に計算できます。DateTime.now()はconstにできません。
Q: どんなときに late を使うべきですか? A: 宣言時点で初期化できないが、使用前に必ず初期化されることが保証できる場合に使ってください。一般的なシナリオは依存性注入や構築後の設定です。誤用は実行時に
LateInitializationErrorを引き起こします。
Q: dynamic と Object? の違いは何ですか? A:
dynamicはすべての型チェックを無効にし、コンパイラはメソッド呼び出しを検証しません。Object?は型チェックを保持し、Objectのメソッドのみ呼び出せます。柔軟性が必要なときはObject?を推奨します。
Q: Dart の int は Web と VM で同じですか? A: 正確には同じではありません。Dart VM では
intは 64 ビット整数です。JS にコンパイルされると、intは JS Number の制限(53 ビット精度)の影響を受けます。大きな数値演算には注意が必要です。
Q: String にシングルクォートとダブルクォートのどちらを使えますか? A: どちらも問題なく、全く同じ効果があります。公式 Dart スタイルガイドではシングルクォートが推奨され、文字列自体にシングルクォートを含む場合のみダブルクォートを使います。
Q: num 型はいつ使うべきですか? A: パラメータや変数が
intとdoubleの両方を受け入れる必要がある場合に使ってください。numはintとdoubleのスーパークラスで、基本的な算術演算をサポートします。
📖 まとめ
- 厳しいものから緩いものまで 5 つの宣言方法:const → final → var → late → dynamic
constはコンパイル時決定、finalは実行時決定だが 1 回だけ代入、varはミュータブルで型を推論- 組み込み型:
int(整数)、double(浮動小数点)、String(文字列 + 補間)、bool(ブール) - ローカル変数の推論には
var/finalを使い、公開 API には明示的に型を付ける - DataPipeline では
constで設定定数を定義し、finalで実行時不変値を定義する
📝 練習問題
- 基礎(難易度 ⭐):次の変数を宣言してください:
constのアプリケーション名、finalの現在時刻、varのカウンター。それぞれの値と型を表示しましょう。 - 中級(難易度 ⭐⭐):
num型のパラメータを受け取り、その数値を USD 形式の文字列にして返す関数を書いてください(例:formatUSD(1500.5)は"$1,500.50 USD"を返す)。intとdoubleの両方の入力を扱います。 - 上級(難易度 ⭐⭐⭐):
lateを使ってデータベース接続文字列を遅延初期化し、constでデフォルト値を定義し、finalで環境変数から読み込んだ設定を保存するConfigクラスを設計してください。この 3 つの宣言方法の連携を実演しましょう。