Dart メタプログラミングとリフレクション — アノテーションコード
メタプログラミングはコードを書くコードである — 反復労働を自動化し、開発者がビジネスロジックに集中できるようにする。
1. 学べること
- dart:mirrors リフレクション API(VM のみの制限)
- アノテーション:定義と読み取り
- コード生成 vs リフレクションのトレードオフ
- リフレクションレス設計思想と Flutter の選択
- Bob のシナリオ:DataPipeline でのアノテーション駆動フィールドマッピング
2. 開発者のリアルな物語
(1) 課題:手動のシリアライゼーションコードが開発努力の 60% を占める
Bob の DataPipeline には 20 のデータモデルクラスがあり、それぞれに fromJson/toJson メソッドが必要だった。20 クラスのシリアライゼーションコードを手動で書くのに 3 日かかり、エラーが発生しやすかった — たった 1 つのフィールド名のスペルミスで、10 万件のレコードのパース失敗が起きた。
(2) コード生成による解決
Dart はリフレクションではなくコード生成を選んだ。モデルクラスにアノテーションを付けることで、build_runner が自動的にシリアライゼーションコードを生成する。
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- 20 クラスのシリアライゼーションコードが自動生成され、3 日から 30 分に短縮
- フィールド名のスペルミスがコンパイル時に検出される
- AOT コンパイル対応で、全プラットフォームでの Flutter/Web をサポート
3. アノテーション
(1) アノテーションの定義と使用
▶ サンプル:カスタムアノテーション
// カスタムアノテーションを定義
class Column {
final String name;
final bool nullable;
final String? defaultValue;
const Column({
required this.name,
this.nullable = false,
this.defaultValue,
});
}
class Table {
final String name;
const Table(this.name);
}
// クラスにアノテーションを適用
@Table('orders')
class Order {
@Column(name: 'order_id')
final String id;
@Column(name: 'amount', nullable: false)
final double amount;
@Column(name: 'status', defaultValue: 'pending')
final String status;
Order({required this.id, required this.amount, this.status = 'pending'});
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:リフレクションを使ったアノテーション読み取り(VM のみ)
import 'dart:mirrors';
// リフレクションを使ってアノテーションを読み取る(VM のみ!)
void printTableInfo(Type type) {
final classMirror = reflectClass(type);
// クラスレベルのアノテーションを読み取る
for (final metadata in classMirror.metadata) {
if (metadata.reflectee is Table) {
final table = metadata.reflectee as Table;
print('Table: ${table.name}');
}
}
// フィールドレベルのアノテーションを読み取る
classMirror.declarations.forEach((key, declaration) {
if (declaration is VariableMirror) {
for (final metadata in declaration.metadata) {
if (metadata.reflectee is Column) {
final column = metadata.reflectee as Column;
print(' ${declaration.simpleName}: ${column.name} '
'(nullable: ${column.nullable}, default: ${column.defaultValue})');
}
}
}
});
}
void main() {
printTableInfo(Order);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
4. dart:mirrors リフレクション API
(1) リフレクション機能の概要
graph TD A[メタプログラミング] --> B[dart:mirrors リフレクション] A --> C[アノテーション + コード生成] B --> B1[ランタイム柔軟] B --> B2[VM のみ / AOT 利用不可] C --> C1[コンパイル時生成] C --> C2[AOT 対応 / Flutter フレンドリー]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:リフレクション API の使用
import 'dart:mirrors';
class Product {
final String name;
final double price;
String category;
Product({required this.name, required this.price, this.category = 'General'});
String formatPrice() => '\$${price.toStringAsFixed(2)} USD';
double applyDiscount(double rate) => price * (1 - rate);
}
void reflectOnProduct() {
final mirror = reflectClass(Product);
// 全インスタンスメソッドをリスト
print('Methods:');
mirror.instanceMembers.forEach((name, member) {
if (member is MethodMirror && !member.isConstructor && !member.isStatic) {
print(' $name: ${member.returnType.reflectedType}');
}
});
// リフレクションでインスタンスを作成
final instance = mirror.newInstance(
Symbol(''),
[],
{#name: 'Laptop', #price: 1299.99, #category: 'Electronics'},
);
// リフレクションでメソッドを起動
final formatted = instance.invoke(#formatPrice, []);
print('Formatted: ${formatted.reflectee}');
final discounted = instance.invoke(#applyDiscount, [0.1]);
print('Discounted: \$${discounted.reflectee.toStringAsFixed(2)} USD');
}
void main() {
reflectOnProduct();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| リフレクション機能 | API | 説明 |
|---|---|---|
| クラス情報取得 | reflectClass(Type) |
クラス名、メソッド、フィールド |
| インスタンス作成 | newInstance() |
動的インスタンス化 |
| メソッド起動 | invoke() |
動的メソッド呼び出し |
| フィールド読み取り | getField() |
動的プロパティアクセス |
| アノテーション読み取り | .metadata |
メタデータを取得 |
5. コード生成 vs リフレクション
(1) 比較とトレードオフ
| 観点 | dart:mirrors リフレクション | コード生成(build_runner) |
|---|---|---|
| ランタイム | 柔軟、ランタイム決定 | コンパイル時に決定 |
| AOT 対応 | 非対応 | 対応 |
| Flutter サポート | リリースモードで非対応 | 全モードでサポート |
| Web サポート | 非対応 | 対応 |
| パフォーマンス | ランタイムオーバーヘッド | ランタイムオーバーヘッドゼロ |
| 開発者体験 | 生成ステップ不要 | build_runner ステップが必要 |
| デバッグ | 困難(動的ディスパッチ) | シンプル(生成コードが読みやすい) |
6. アノテーション駆動フィールドマッピング
(1) Bob のシナリオ:DataPipeline フィールドマッピング
▶ サンプル:手動アノテーションプロセッサ(コード生成をシミュレート)
// フィールドマッピング用のカスタムアノテーション
class FieldMapping {
final String csvColumn;
final String? defaultValue;
final bool required;
const FieldMapping({
required this.csvColumn,
this.defaultValue,
this.required = true,
});
}
class ModelMapping {
final String tableName;
const ModelMapping(this.tableName);
}
// フィールドマッピングアノテーション付きモデル
@ModelMapping('customers')
class Customer {
@FieldMapping(csvColumn: 'customer_id')
final String id;
@FieldMapping(csvColumn: 'customer_name', defaultValue: 'Unknown')
final String name;
@FieldMapping(csvColumn: 'email', required: false)
final String? email;
@FieldMapping(csvColumn: 'total_spent', defaultValue: '0')
final double totalSpent;
Customer({
required this.id,
required this.name,
this.email,
this.totalSpent = 0,
});
// 手動マッピング(実際のプロジェクトではこれは生成される)
static Customer fromCsvMap(Map<String, String> csvRow) {
return Customer(
id: csvRow['customer_id'] ?? '',
name: csvRow['customer_name'] ?? 'Unknown',
email: csvRow['email'],
totalSpent: double.tryParse(csvRow['total_spent'] ?? '0') ?? 0,
);
}
}
void main() {
final csvRow = {
'customer_id': 'CUST-001',
'customer_name': 'Alice',
'email': 'alice@example.com',
'total_spent': '52500.75',
};
final customer = Customer.fromCsvMap(csvRow);
print('Customer: ${customer.id}, ${customer.name}');
print('Email: ${customer.email ?? "N/A"}');
print('Total: \$${customer.totalSpent.toStringAsFixed(2)} USD');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
7. リフレクションレス設計思想
(1) なぜ Dart はリフレクションを採用しないのか
graph TD A[リフレクション vs コード生成] --> B[リフレクション] A --> C[コード生成] B --> B1[ランタイム柔軟性] B --> B2[AOT 非対応] B --> B3[Flutter/Web ブロック] B --> B4[パフォーマンスオーバーヘッド] C --> C1[コンパイル時確実性] C --> C2[AOT 対応] C --> C3[Flutter/Web フレンドリー] C --> C4[ランタイムコストゼロ]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 設計原則 | 説明 |
|---|---|
| AOT ファースト | Flutter リリースは AOT コンパイルを使用;リフレクションは利用不可 |
| ツリーシェイク | コンパイラが未使用コードを削除;リフレクションはツリーシェイクを妨げる |
| パフォーマンスファースト | リフレクションにはランタイムオーバーヘッドがある;コード生成にはオーバーヘッドがない |
| 型安全性 | コード生成は型安全性を維持;リフレクションは型チェックを失う |
▶ サンプル:リフレクションの代替としてのコード生成
// リフレクションベースの JSON パースの代わりに:
// dynamic parseJson(Map<String, dynamic> json, Type type) { ... } // 悪い
// コード生成ベースのアプローチを使う:
// 1. アノテーション付きモデルを定義
// @JsonSerializable()
// class Order { ... }
// 2. 実行: dart run build_runner build
// 3. 生成されたコードが型安全なパースを提供:
// factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
// 手動バージョン(生成コードをシミュレート)
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
// 「生成された」fromJson
factory Order.fromJson(Map<String, dynamic> json) => Order(
id: json['id'] as String,
amount: (json['amount'] as num).toDouble(),
);
// 「生成された」toJson
Map<String, dynamic> toJson() => {
'id': id,
'amount': amount,
};
}
void main() {
final json = {'id': 'ORD-001', 'amount': 1500.0};
final order = Order.fromJson(json); // 型安全!
print(order.toJson());
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. 完全なサンプル:DataPipeline アノテーション駆動マッピング
// ============================================
// DataPipeline アノテーション駆動マッピング
// CSV からモデルへの変換のためのコード生成をシミュレート
// ============================================
// アノテーション
class CsvField {
final String column;
final String? defaultValue;
final bool required;
const CsvField({
required this.column,
this.defaultValue,
this.required = true,
});
}
class CsvModel {
final String fileName;
const CsvModel(this.fileName);
}
// モデルクラス
@CsvModel('orders')
class Order {
@CsvField(column: 'order_id')
final String id;
@CsvField(column: 'total_amount', defaultValue: '0')
final double amount;
@CsvField(column: 'order_status', defaultValue: 'pending')
final String status;
@CsvField(column: 'category', required: false)
final String? category;
Order({
required this.id,
required this.amount,
this.status = 'pending',
this.category,
});
// 実際のプロジェクトでは、これは build_runner で生成される
static Order fromCsvRow(Map<String, String> row) => Order(
id: row['order_id'] ?? '',
amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
status: row['order_status'] ?? 'pending',
category: row['category'],
);
double get tax => amount * 0.08;
double get total => amount + tax;
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status${category != null ? ", $category" : ""})';
}
// マッパー(生成コードをシミュレート)
class CsvMapper<T> {
final T Function(Map<String, String>) fromCsvRow;
CsvMapper(this.fromCsvRow);
List<T> mapAll(List<Map<String, String>> rows) =>
rows.map(fromCsvRow).toList();
(List<T> valid, List<(int, String)> errors) mapSafe(
List<Map<String, String>> rows) {
final valid = <T>[];
final errors = <(int, String)>[];
for (var i = 0; i < rows.length; i++) {
try {
valid.add(fromCsvRow(rows[i]));
} catch (e) {
errors.add((i + 1, e.toString()));
}
}
return (valid, errors);
}
}
void main() {
// シミュレートされた CSV データ(すでに Map にパース済み)
final csvRows = <Map<String, String>>[
{'order_id': 'ORD-001', 'total_amount': '1500.00', 'order_status': 'completed', 'category': 'Electronics'},
{'order_id': 'ORD-002', 'total_amount': '3200.50', 'order_status': 'completed', 'category': 'Electronics'},
{'order_id': 'ORD-003', 'total_amount': '890.00', 'order_status': 'pending', 'category': 'Clothing'},
{'order_id': 'ORD-004', 'total_amount': '50.00', 'order_status': 'completed'},
];
// モデルオブジェクトにマップ
final mapper = CsvMapper<Order>(Order.fromCsvRow);
final (valid, errors) = mapper.mapSafe(csvRows);
print('=== DataPipeline CSV Import Report ===');
print('Rows: ${csvRows.length}');
print('Valid: ${valid.length}');
print('Errors: ${errors.length}');
if (errors.isNotEmpty) {
print('\nErrors:');
for (final (line, msg) in errors) {
print(' Line $line: $msg');
}
}
// 有効な注文を処理
double totalRevenue = 0;
for (final order in valid) {
if (order.status == 'completed') {
totalRevenue += order.total;
}
print(' $order');
}
print('\nRevenue (completed): \$${totalRevenue.toStringAsFixed(2)} USD');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline CSV Import Report ===
Rows: 4
Valid: 4
Errors: 0
Order(ORD-001, $1500.00, completed, Electronics)
Order(ORD-002, $3200.50, completed, Electronics)
Order(ORD-003, $890.00, pending, Clothing)
Order(ORD-004, $50.00, completed)
Revenue (completed): $5132.54 USD
❓ よくある質問
Q: なぜ Flutter は dart:mirrors をサポートしないのですか? A: Flutter はネイティブコードへの AOT コンパイルを使用します。AOT はすべての型情報がコンパイル時に決定されている必要があります。リフレクションはランタイムで型を動的に検索しますが、これは AOT のツリーシェイクとコンパイル最適化と競合します。
Q: アノテーション自体は何の役に立ちますか? A: アノテーション自体はロジックを実行せず、純粋なメタデータです。リフレクション(VM)またはコードジェネレータ(build_runner)で読み取られて初めて有効になります。
Q: コードを変更するたびに build_runner を再実行する必要がありますか? A: はい、ただしファイルの変更を監視して自動再生成する
--watchモードがあります。開発中は watch モードを、リリース前は ビルド モードを使用してください。
Q: json_serializable と手動で fromJson を書くことの違いは何ですか? A: json_serializable は自動的にコードを生成し、手動書き込みのエラーを回避し、ネストされたオブジェクトとカスタム変換をサポートします。手動書き込みはシンプルですがエラーが発生しやすく、ネストされたオブジェクトはさらに苦痛になります。
Q: Dart は将来マクロをサポートするようになりますか? A: Dart チームはマクロシステムを開発中(macro パッケージ)で、build_runner の一部を置き換えてより良い開発者体験を提供することを目指しています。ただし、まだ安定していません。
Q: コード生成はパッケージサイズを増やしますか? A: はい、生成されたコードはコンパイル出力に含まれるためです。ただし、リフレクションもサイズを増やします(ツリーシェイクを防ぐため);差異は最小限です。
Q: 生成されたコードをデバッグするにはどうすればよいですか? A: 生成された .g.dart ファイルを直接開いてデバッグできます。build_runner が生成するコードは標準の Dart コードなので、ブレークポイントを設定できます。
📖 まとめ
- dart:mirrors はランタイムリフレクション機能を提供するが、VM 環境に限定され、Flutter AOT と Web では利用できない
- アノテーションはメタデータマーカーであり、リフレクションまたはコードジェネレータで消費される必要がある
- Dart エコシステムは AOT/Flutter/Web との互換性のために「リフレクション」ではなく「コード生成」を選択した
- リフレクションレス設計思想:コンパイル時の決定性、ランタイムオーバーヘッドゼロ、ツリーシェイクの維持
- DataPipeline は @CsvField アノテーションを使ってフィールドマッピングをマークし、コード生成をシミュレートする
📝 練習問題
- 基礎(難易度 ⭐):3 つのカスタムアノテーション(@ApiEndpoint、@Required、@DefaultValue)を定義し、クラスに適用してください。dart:mirrors を使ってアノテーション情報を読み取ります(注意:VM 上でのみ実行可能)。
- 中級(難易度 ⭐⭐):json_serializable プロジェクトを作成し、5 つのフィールドを持つ Order クラスの fromJson/toJson コードを生成してください。生成された .g.dart ファイルを確認し、生成ロジックを理解します。
- 挑戦(難易度 ⭐⭐⭐):シンプルなコードジェネレータを設計してください:@CsvField でアノテーションされたクラス定義を読み取り、静的な fromCsvRow メソッドを生成します。ヒント:source_gen パッケージまたはシンプルな文字列テンプレートを使用できます。