Dart ジェネリクス — 型安全で再利用可能なプログラミング
ジェネリクスはコードのテンプレートである — 一度書けば多くの型で再利用でき、コンパイラが型安全を保証する。
1. 学べること
- ジェネリッククラスとジェネリック関数の定義
- ジェネリック制約:
extendsと複数の境界 - ジェネリクスとコレクションの連携
- 共変性と反変性(型の境界)
- Bob のシナリオ:DataPipeline のジェネリックデータ変換器
DataTransformer<T, R>
2. 開発者のリアルな物語
(1) 課題:重複コードによる保守の悪夢
Charlie は DataPipeline で各データ型ごとに別々のパーサーを書いた:OrderParser、ProductParser、CustomerParser。3 つのパーサーのロジックはほぼ同じで(読み込み → 検証 → 変換)、入出力型だけが異なっていた。パースロジックに変更が必要なたびに、3 か所を修正する必要があった。あるとき、変更漏れで Product パーサーが新しい検証ルールを適用しなかった。
(2) ジェネリックによる解決
ジェネリッククラス DataTransformer<T, R> を使って 3 つのパーサーを統合する。型パラメータによりコンパイラが型安全を保証し、コードは 1 か所で保守すればよくなる。
abstract class DataTransformer<T, R> {
R transform(T input);
List<R> batch(List<T> inputs) => inputs.map(transform).toList();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- 3 つのパーサーが 1 つのジェネリッククラスに統合され、コード量が 70% 削減
- 検証ロジックの変更は 1 か所だけで済み、すべての型が自動的に更新される
- コンパイラが型安全を保証し、Order と Product の混同を防ぐ
3. ジェネリクスの基礎
(1) なぜジェネリクスが必要か
▶ サンプル:ジェネリクスがない場合の問題
// ジェネリクスなし - List<dynamic> は型安全性を失う
void main() {
List amounts = [1500, 3200, 890]; // List<dynamic>
amounts.add('not a number'); // コンパイルエラーなし!
for (final a in amounts) {
print((a as int) * 2); // 'not a number' でランタイムクラッシュ
}
}
// ジェネリクスあり - コンパイル時の型安全性
void main() {
List<int> amounts = [1500, 3200, 890];
// amounts.add('not a number'); // コンパイルエラー!
for (final a in amounts) {
print(a * 2); // 安全 - 型が判明
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
4. ジェネリッククラス
(1) ジェネリッククラス定義
▶ サンプル:ジェネリックコンテナクラス
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
Result.success(this.data)
: error = null,
isSuccess = true;
Result.failure(this.error)
: data = null,
isSuccess = false;
// ジェネリック型を返すメソッド
T get dataOrThrow {
if (isSuccess && data != null) return data;
throw Exception(error ?? 'Unknown error');
}
// 成功値を変換
Result<R> map<R>(R Function(T) fn) {
if (isSuccess && data != null) {
return Result.success(fn(data));
}
return Result.failure(error);
}
}
void main() {
final success = Result.success(1500.0);
final failure = Result<double>.failure('Network timeout');
print(success.data); // 1500.0
print(failure.error); // Network timeout
// map で成功値を変換
final formatted = success.map((v) => '\$${v.toStringAsFixed(2)} USD');
print(formatted.data); // $1500.00 USD
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:ジェネリックキャッシュクラス
class Cache<T> {
final Map<String, T> _store = {};
final Duration _ttl;
Cache({Duration ttl = const Duration(minutes: 5)}) : _ttl = ttl;
void put(String key, T value) => _store[key] = value;
T? get(String key) => _store[key];
bool contains(String key) => _store.containsKey(key);
void remove(String key) => _store.remove(key);
void clear() => _store.clear();
}
void main() {
final orderCache = Cache<Map<String, dynamic>>();
orderCache.put('ORD-001', {'amount': 1500.0, 'status': 'completed'});
print(orderCache.get('ORD-001')); // {amount: 1500.0, status: completed}
print(orderCache.contains('ORD-002')); // false
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
5. ジェネリック関数
▶ サンプル:ジェネリック関数定義
// 単一型パラメータのジェネリック関数
T firstOrNull<T>(List<T> items) => items.isEmpty ? throw StateError('Empty list') : items.first;
// 2 つの型パラメータのジェネリック関数
R transform<T, R>(T input, R Function(T) converter) => converter(input);
// 制約付きジェネリック関数
double sumNumbers<T extends num>(List<T> items) =>
items.fold(0.0, (sum, item) => sum + item.toDouble());
void main() {
print(firstOrNull(['ORD-001', 'ORD-002'])); // ORD-001
print(firstOrNull([1500, 3200])); // 1500
final formatted = transform(1500.0, (v) => '\$${v} USD');
print(formatted); // $1500.0 USD
print(sumNumbers([1, 2, 3, 4, 5])); // 15.0
print(sumNumbers([1.5, 2.5, 3.0])); // 7.0
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:ジェネリックユーティリティ関数
// 安全なキャストユーティリティ
T? tryCast<T>(dynamic value) {
if (value is T) return value;
return null;
}
// リストを 2 つのグループに分割
(List<T>, List<T>) partition<T>(List<T> items, bool Function(T) predicate) {
final matching = <T>[];
final notMatching = <T>[];
for (final item in items) {
(predicate(item) ? matching : notMatching).add(item);
}
return (matching, notMatching);
}
void main() {
// 安全なキャスト
final intVal = tryCast<int>('hello'); // null
final strVal = tryCast<String>('hello'); // hello
// 分割
final (high, low) = partition([1500.0, 50.0, 3200.0, 890.0], (v) => v >= 1000);
print('High: $high'); // [1500.0, 3200.0]
print('Low: $low'); // [50.0, 890.0]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
6. ジェネリック制約
(1) 単一の extends 制約
▶ サンプル:extends 制約
// num を拡張する型のみ受け入れる
double average<T extends num>(List<T> values) {
if (values.isEmpty) return 0;
final sum = values.fold<num>(0, (a, b) => a + b);
return sum / values.length;
}
// Comparable を実装する型のみ受け入れる
T findMax<T extends Comparable>(List<T> items) {
if (items.isEmpty) throw StateError('Empty list');
return items.reduce((a, b) => a.compareTo(b) >= 0 ? a : b);
}
void main() {
print(average([10, 20, 30])); // 20.0
print(average([1.5, 2.5, 3.0])); // 2.333...
print(findMax(['banana', 'apple', 'cherry'])); // cherry
print(findMax([10, 5, 8])); // 10
// average(['a', 'b']); // コンパイルエラー!String は num を拡張しない
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) 複数の制約
▶ サンプル:複数の制約
// 交差型を使った複数の制約
class SortedCollection<T extends Comparable<T>> {
final List<T> _items = [];
void add(T item) {
final index = _items.indexWhere((e) => item.compareTo(e) <= 0);
if (index == -1) {
_items.add(item);
} else {
_items.insert(index, item);
}
}
List<T> get items => List.unmodifiable(_items);
}
void main() {
final sorted = SortedCollection<String>();
sorted.add('cherry');
sorted.add('apple');
sorted.add('banana');
print(sorted.items); // [apple, banana, cherry]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
7. ジェネリクスとコレクションの連携
▶ サンプル:型安全なコレクション操作
// 型安全なキーバリューストア
class KeyValueStore<K, V> {
final Map<K, V> _data = {};
void put(K key, V value) => _data[key] = value;
V? get(K key) => _data[key];
// 全値を変換
Map<K, R> mapValues<R>(R Function(V) converter) {
return _data.map((key, value) => MapEntry(key, converter(value)));
}
// キー型でフィルタ
Map<K, V> whereKey(bool Function(K) predicate) {
return Map.fromEntries(
_data.entries.where((e) => predicate(e.key)),
);
}
}
void main() {
final store = KeyValueStore<String, double>();
store.put('Electronics', 3600.0);
store.put('Books', 170.0);
store.put('Clothing', 890.0);
// 値をフォーマット済み文字列にマップ
final formatted = store.mapValues((v) => '\$${v.toStringAsFixed(2)} USD');
print(formatted); // {Electronics: $3600.00 USD, Books: $170.00 USD, Clothing: $890.00 USD}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. Bob のシナリオ:ジェネリックデータ変換器
classDiagram
class DataTransformer~T, R~ {
+transform(T input) R
+batch(List~T~ inputs) List~R~
+validate(T input) bool
}
class OrderParser {
+transform(String) Order
}
DataTransformer <|-- OrderParser
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:DataTransformer ジェネリックフレームワーク
// ベース変換器インターフェース
abstract class DataTransformer<T, R> {
R transform(T input);
String get name;
List<R> batch(List<T> inputs) {
final results = <R>[];
final errors = <(int, String)>[];
for (var i = 0; i < inputs.length; i++) {
try {
if (validate(inputs[i])) {
results.add(transform(inputs[i]));
}
} catch (e) {
errors.add((i, e.toString()));
}
}
if (errors.isNotEmpty) {
print('$name: ${errors.length} errors during batch transform');
}
return results;
}
bool validate(T input) => true;
}
// 具象変換器:String CSV 行 → Order
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)})';
}
class CsvOrderTransformer extends DataTransformer<String, Order> {
@override
String get name => 'CsvOrderTransformer';
@override
bool validate(String input) {
final parts = input.split(',');
return parts.length >= 2;
}
@override
Order transform(String input) {
final parts = input.split(',');
return Order(
id: parts[0].trim(),
amount: double.parse(parts[1].trim()),
);
}
}
void main() {
final transformer = CsvOrderTransformer();
final csvLines = [
'ORD-001, 1500.0',
'ORD-002, 3200.0',
'ORD-003, 890.0',
];
final orders = transformer.batch(csvLines);
print('Transformed ${orders.length} orders:');
for (final order in orders) {
print(' $order');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
9. 完全なサンプル:DataPipeline ジェネリック処理フレームワーク
// ============================================
// DataPipeline ジェネリック処理フレームワーク
// 型安全なデータ変換パイプライン
// ============================================
// 安全なエラー処理のための Result 型
class Result<T> {
final T? data;
final String? error;
Result.success(this.data) : error = null;
Result.failure(this.error) : data = null;
bool get isSuccess => data != null;
Result<R> map<R>(R Function(T) fn) {
if (isSuccess && data != null) return Result.success(fn(data!));
return Result.failure(error);
}
}
// ジェネリック変換器
abstract class DataTransformer<T, R> {
R transform(T input);
String get name;
bool validate(T input) => true;
Result<R> safeTransform(T input) {
try {
if (!validate(input)) {
return Result.failure('$name: Validation failed');
}
return Result.success(transform(input));
} catch (e) {
return Result.failure('$name: $e');
}
}
List<Result<R>> batch(List<T> inputs) =>
inputs.map(safeTransform).toList();
}
// ジェネリック集約器
abstract class DataAggregator<T, R> {
R aggregate(List<T> items);
String get name;
}
// 数値型の合計集約器
class SumAggregator<T extends num> extends DataAggregator<T, double> {
@override
String get name => 'SumAggregator';
@override
double aggregate(List<T> items) =>
items.fold(0.0, (sum, item) => sum + item.toDouble());
}
// 平均集約器
class AverageAggregator<T extends num> extends DataAggregator<T, double> {
@override
String get name => 'AverageAggregator';
@override
double aggregate(List<T> items) =>
items.isEmpty ? 0 : items.fold(0.0, (s, i) => s + i.toDouble()) / items.length;
}
// 変換器をチェーンするパイプライン
class Pipeline<I, M, O> {
final DataTransformer<I, M> _first;
final DataTransformer<M, O> _second;
Pipeline(this._first, this._second);
List<Result<O>> process(List<I> inputs) {
final midResults = _first.batch(inputs);
final midValues = midResults.where((r) => r.isSuccess).map((r) => r.data!).toList();
return _second.batch(midValues);
}
}
// 具象型
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)})';
}
class OrderSummary {
final String id;
final String tier;
OrderSummary({required this.id, required this.tier});
@override
String toString() => 'OrderSummary($id, $tier)';
}
class CsvToOrder extends DataTransformer<String, Order> {
@override
String get name => 'CsvToOrder';
@override
Order transform(String input) {
final parts = input.split(',');
return Order(id: parts[0].trim(), amount: double.parse(parts[1].trim()));
}
@override
bool validate(String input) => input.split(',').length >= 2;
}
class OrderToSummary extends DataTransformer<Order, OrderSummary> {
@override
String get name => 'OrderToSummary';
@override
OrderSummary transform(Order input) {
final tier = input.amount >= 1000 ? 'Premium' : 'Standard';
return OrderSummary(id: input.id, tier: tier);
}
}
void main() {
final csvLines = [
'ORD-001, 1500.0',
'ORD-002, 50.0',
'ORD-003, 3200.0',
];
// シンプルな変換器
final parser = CsvToOrder();
final orders = parser.batch(csvLines);
print('=== Parsed Orders ===');
for (final result in orders) {
if (result.isSuccess) {
print(' ${result.data}');
} else {
print(' Error: ${result.error}');
}
}
// パイプライン:CSV → Order → Summary
final pipeline = Pipeline<String, Order, OrderSummary>(
CsvToOrder(), OrderToSummary());
final summaries = pipeline.process(csvLines);
print('\n=== Order Summaries ===');
for (final result in summaries) {
if (result.isSuccess) print(' ${result.data}');
}
// 集約
final validAmounts = orders
.where((r) => r.isSuccess)
.map((r) => r.data!.amount)
.toList();
final total = SumAggregator().aggregate(validAmounts);
final avg = AverageAggregator().aggregate(validAmounts);
print('\nRevenue: \$${total.toStringAsFixed(2)} USD');
print('Average: \$${avg.toStringAsFixed(2)} USD');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== Parsed Orders ===
Order(ORD-001, $1500.00)
Order(ORD-002, $50.00)
Order(ORD-003, $3200.00)
=== Order Summaries ===
OrderSummary(ORD-001, Premium)
OrderSummary(ORD-002, Standard)
OrderSummary(ORD-003, Premium)
Revenue: $4750.00 USD
Average: $1583.33 USD
❓ よくある質問
Q: ジェネリック型パラメータは実行時に存在しますか? A: はい、Dart のジェネリクスはリフレクション可能です。実行時に型情報を取得できます。
list is List<int>は実行時に true を返します(Java の型消去とは異なります)。
Q: ジェネリック制約に複数の境界を持たせられますか? A: はい、
T extends A & B構文を使えます(ただし Dart は現在&での複数境界を直接サポートしていません)。制約付き型に複数のインターフェースを実装させるか、T extends Comparable<num>のような境界を使うことで間接的に実現できます。
Q: ジェネリクスと dynamic はいつ使い分けるべきですか? A: 可能な限りジェネリクスを使ってください。ジェネリクスはコンパイル時に型をチェックしますが、
dynamicはエラーを実行時にしか明らかにしません。型が本当に不明な場合(JSON パースなど)のみdynamicを使ってください。
Q: ジェネリックメソッドはクラスとは異なる型パラメータを持てますか? A: はい。例えば
<T, R> R transform(T input, R Function(T) fn)は 2 つの型パラメータを持ちます。メソッドの型パラメータはクラスの型パラメータとは独立しています。
Q:
List<dynamic>とList<T>の違いは何ですか? A:List<dynamic>は任意の型の要素を受け入れ、型安全性を失います。List<T>は型 T の要素のみを受け入れ、コンパイラが保証する型安全性を持ちます。List<dynamic>は他のList<T>型のスーパークラスではありません。
Q: ジェネリック制約における
extendsとimplementsの違いはありますか? A: 制約では、extendsはクラスの継承とインターフェース実装の両方にマッチします。T extends Comparableは T が Comparable のサブクラスまたは実装クラスになれることを意味します。
Q:
covariantキーワードは何に使われますか? A:covariantはサブタイプがパラメータに対してより具体的な型を使用できるようにします(共変性)。コンシューマパターンでパラメータによく使われますが、型安全性を弱めるため慎重に使ってください。
📖 まとめ
- ジェネリクスは型パラメータ
<T>を介してコードを複数の型で再利用できるようにし、コンパイラが型安全を保証する。 - ジェネリッククラス(
class Box<T>)とジェネリック関数(T first<T>(List<T>))は最も一般的な形式。 extends制約は型パラメータの範囲を制限する。例:<T extends num>は数値型のみ受け入れる。- ジェネリックコレクション(
List<T>、Map<K, V>)はList<dynamic>より安全。 - DataPipeline の
DataTransformer<T, R>はジェネリック設計の典型的な応用。
📝 練習問題
- 基礎(難易度 ⭐):
firstとsecondフィールドを持つジェネリックPair<T, U>クラスと、値を入れ替えた新しい Pair を返すswap()メソッドを実装してください。Pair<int, String>とPair<double, bool>でテストします。 - 中級(難易度 ⭐⭐):
isSuccess、data、error、およびmap<R>()メソッドを持つResult<T>型(Rust の Result に類似)を実装してください。失敗する可能性のあるパース関数を書き直すのに使います。 - 挑戦(難易度 ⭐⭐⭐):複数の
DataTransformerの連鎖をサポートし、中間型を自動的に推論し、最後にprocess()を呼び出してパイプライン全体を実行するジェネリックPipeline<T, R>クラスを実装してください。ヒント:再帰型またはビルダーパターンが使えます。