Dart ジェネリクス
ジェネリクス(Generics)を使用すると、コードを書くときに具体的な型を指定せず、型パラメータをプレースホルダとして使用し、使用時に型を決定できます。
ジェネリクスの核心的な価値は型安全性とコードの再利用——1つのコードで複数の型に対応し、同時にコンパイラが型エラーをチェックしてくれます。
なぜジェネリクスが必要なのか
まず、ジェネリクスがない場合の課題を見てみましょう。
例
ジェネリクスを使わない書き方:
class IntBox {
int value;
IntBox(this.value);
}
class StringBox {
String value;
StringBox(this.value);
}
// 新しい型を追加するたびに新しい Box クラスを書く必要があり、コードが大量に重複する
void main() {
var intBox = IntBox(42);
var stringBox = StringBox('サンプル');
print('整数: ${intBox.value}');
print('文字列: ${stringBox.value}');
}
int も String も格納でき、しかもコンパイル時に型安全性を保証できる Box クラスを書く方法はないでしょうか?
これがジェネリクスが解決する問題です。
ジェネリクスクラス
ジェネリクスクラスは、クラス名の後に山かっこ <> で型パラメータを宣言し、クラス内部でこの型パラメータを使用します。
例
// 慣例のな命名:T(Type)、E(Element)、K(Key)、V(Value)
class Box<T> {
T value;
Box(this.value);
T getValue() {
return value;
}
void setValue(T newValue) {
value = newValue;
}
void printValue() {
print('Box 内の値: $value (型: ${value.runtimeType})');
}
}
void main() {
// ジェネリクスクラスを使用するときに具体的な型を指定
var intBox = Box<int>(42);
var stringBox = Box<String>('サンプル Dart チュートリアル');
var doubleBox = Box<double>(3.14);
intBox.printValue();
stringBox.printValue();
doubleBox.printValue();
// 型安全性:以下のコードはコンパイル時にエラーになる
// intBox.setValue('hello'); // エラー:String は int に代入できない
// 型推論:Dart はコンストラクタの引数から型を推論できる
var autoBox = Box('String として自動推論');
print('推論された型: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int) Box 中的值: サンプル Dart 教程 (类型: String) Box 中的值: 3.14 (类型: double) 推断类型: String
複数の型パラメータ
ジェネリクスクラスは複数の型パラメータを持つことができます。
例
class Pair<K, V> {
K key;
V value;
Pair(this.key, this.value);
@override
String toString() => 'Pair($key: $value)';
}
void main() {
var pair1 = Pair<String, int>('サンプル', 10);
var pair2 = Pair('score', 95.5); // 型推論
print(pair1);
print(pair2);
}
Pair(サンプル: 10) Pair(score: 95.5)
ジェネリクスメソッド
ジェネリクスクラスに加えて、関数やメソッドでもジェネリクスを使用できます。
例
// 戻り値の型の前に <T> を宣言
T firstElement<T>(List<T> list) {
if (list.isEmpty) {
throw ArgumentError('リストを空にすることはできません');
}
return list[0];
}
// ジェネリクス関数:2つの値を交換
void swap<T>(List<T> list, int i, int j) {
T temp = list[i];
list[i] = list[j];
list[j] = temp;
}
void main() {
// 呼び出し時に Dart が自動的に型を推論
var names = ['サンプル', 'Dart', 'Flutter'];
print('最初の名前: ${firstElement(names)}');
var scores = [95, 88, 72];
print('最初のスコア: ${firstElement(scores)}');
// 要素の交換
var items = ['A', 'B', 'C'];
print('交換前: $items');
swap(items, 0, 2);
print('交換後: $items');
}
第一个名字: サンプル 第一个分数: 95 交换前: [A, B, C] 交换后: [C, B, A]
型制約 extends
型パラメータを特定の型のサブタイプのみに制限したい場合があります。
extends キーワードを使用すると、ジェネリクスパラメータに制約を課すことができます。
例
// <T extends num> は T が num またはそのサブクラス(int、double)でなければならないという制約
class Calculator<T extends num> {
T a;
T b;
Calculator(this.a, this.b);
// T extends num という制約があるため、num のメソッドを安全に使用できる
double add() => (a + b).toDouble();
double subtract() => (a - b).toDouble();
double multiply() => (a * b).toDouble();
double divide() => a / b;
void printOperations() {
print('サンプル 計算機: $a と $b');
print(' 加算: ${add()}');
print(' 減算: ${subtract()}');
print(' 乗算: ${multiply()}');
print(' 除算: ${divide().toStringAsFixed(2)}');
}
}
void main() {
var intCalc = Calculator<int>(10, 3);
intCalc.printOperations();
var doubleCalc = Calculator<double>(3.5, 1.5);
doubleCalc.printOperations();
// 以下の行はエラーになる:String は num のサブクラスではない
// var strCalc = Calculator<String>('a', 'b'); // エラー!
}
サンプル 计算器: 10 和 3 加: 13.0 减: 7.0 乘: 30.0 除: 3.33 サンプル 计算器: 3.5 和 1.5 加: 5.0 减: 2.0 乘: 5.25 除: 2.33
型制約により、ジェネリクスの柔軟性を保ちながら、制約された型のメソッドを使用できます。例えば上記で T extends num と制約した場合、num 型がこれらの演算をサポートしているため、メソッド本体内で +、-、*、/ などの演算子を使用できます。
ジェネリクスコレクションの使用
Dart の主要なコレクションクラス(List、Set、Map)はすべてジェネリクスです。
ジェネリクスコレクションを使用すると、コンパイル時の型チェックが可能になります。
例
// 要素の型を String に指定
List<String> names = ['サンプル', 'Dart', 'Flutter'];
// names.add(42); // エラー:int は List<String> に追加できない
// キーと値の型を指定
Map<String, int> scores = {
'サンプル': 95,
'Alice': 87,
};
// scores['Bob'] = '优秀'; // エラー:String は int に代入できない
// Set の要素型を指定
Set<double> prices = {9.99, 19.99, 29.99};
// ジェネリクスコレクションの関数型操作
List<int> numbers = [1, 2, 3, 4, 5, 6];
// 型安全なフィルタリングとマッピング
List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
List<String> labels = numbers.map((n) => 'サンプル-$n').toList();
print('偶数: $evenNumbers');
print('ラベル: $labels');
// fold を使用した型安全な累積計算
int sum = numbers.fold<int>(0, (prev, n) => prev + n);
print('合計: $sum');
}
偶数: [2, 4, 6] 标签: [サンプル-1, サンプル-2, サンプル-3, サンプル-4, サンプル-5, サンプル-6] 总和: 21
実践応用:ジェネリクスリポジトリパターン
ジェネリクスの実開発における最も古典的な応用の1つが「リポジトリパターン」(Repository Pattern)です。
例
abstract class Repository<T> {
void add(T item);
void remove(T item);
T? findById(String id);
List<T> getAll();
}
// ユーザーモデル
class User {
String id;
String name;
User(this.id, this.name);
@override
String toString() => 'User($id, $name)';
}
// 商品モデル
class Product {
String id;
String name;
double price;
Product(this.id, this.name, this.price);
@override
String toString() => 'Product($name, ¥$price)';
}
// 具体的な実装:ユーザーリポジトリ
class UserRepository implements Repository<User> {
final List<User> _users = [];
@override
void add(User user) => _users.add(user);
@override
void remove(User user) => _users.remove(user);
@override
User? findById(String id) {
try {
return _users.firstWhere((u) => u.id == id);
} catch (_) {
return null;
}
}
@override
List<User> getAll() => List.unmodifiable(_users);
}
// 具体的な実装:商品リポジトリ(同じインターフェース、異なる型)
class ProductRepository implements Repository<Product> {
final List<Product> _products = [];
@override
void add(Product product) => _products.add(product);
@override
void remove(Product product) => _products.remove(product);
@override
Product? findById(String id) {
try {
return _products.firstWhere((p) => p.id == id);
} catch (_) {
return null;
}
}
@override
List<Product> getAll() => List.unmodifiable(_products);
}
void main() {
var userRepo = UserRepository();
userRepo.add(User('u1', 'サンプル'));
userRepo.add(User('u2', 'Alice'));
var productRepo = ProductRepository();
productRepo.add(Product('p1', 'Dart チュートリアル', 29.99));
productRepo.add(Product('p2', 'Flutter チュートリアル', 49.99));
print('ユーザーリスト: ${userRepo.getAll()}');
print('商品リスト: ${productRepo.getAll()}');
print('ユーザー u1 を検索: ${userRepo.findById('u1')}');
print('商品 p2 を検索: ${productRepo.findById('p2')}');
}
用户列表: [User(u1, サンプル), User(u2, Alice)] 商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)] 查找用户 u1: User(u1, サンプル) 查找商品 p2: Product(Flutter 教程, ¥49.99)
ジェネリクスリポジトリパターンを使用すると、統一されたインターフェースで異なる型のデータを処理でき、型安全性を保証しつつ、重複コードを回避できます。
その他の拡張