はじめに
Dart の「ジェネリクス(Generics)」は、型安全性を高めながら再利用性の高いコードを書くための仕組みです。
List や Map のような汎用コレクションがまさにその代表例です。
なぜジェネリクスが必要なのか?
型を明示しないと、Dart は dynamic として扱います。
すると、実行時エラーのリスクが増えます。
List items = [1, 2, 3];
items.add("hello"); // ⚠️ 実行時まで気づけない
int first = items[0]; // 実行時に型エラー!
これをジェネリクスで型を固定すれば:
List<int> numbers = [1, 2, 3];
// numbers.add("hello"); ❌ コンパイルエラーで防げる!
メリット
- 型安全(コンパイル時にエラー検出)
- コード補完が効く(IDEサポート強化)
- 再利用性が高い
基本構文
class Box<T> {
T value;
Box(this.value);
}
void main() {
var intBox = Box<int>(123);
var strBox = Box<String>("Hello");
print(intBox.value); // 123
print(strBox.value); // Hello
}
ここでの <T> が「型パラメータ」です。
T は任意の型を受け取る プレースホルダー です。
型パラメータ名の慣習
| パラメータ | 意味 |
|---|---|
T |
Type(一般的な型) |
E |
Element(コレクションの要素) |
K |
Key(Mapのキー) |
V |
Value(Mapの値) |
例:
Map<K, V> getDefaultMap<K, V>(K key, V value) {
return {key: value};
}
型制約(Type Constraints)
ジェネリクスには「上限を設ける(extends制約)」ことも可能です。
class Animal {
void speak() => print("Animal sound");
}
class Dog extends Animal {
void speak() => print("Woof!");
}
class Cage<T extends Animal> {
T animal;
Cage(this.animal);
void makeSound() => animal.speak();
}
void main() {
var dogCage = Cage(Dog());
dogCage.makeSound(); // Woof!
// var cage = Cage<String>("hello"); ❌ コンパイルエラー
}
T extends Animal により、T は Animal またはそのサブクラスに限定。
ジェネリクスメソッド
クラスだけでなく、関数にもジェネリクスを使えます。
T getFirst<T>(List<T> items) {
return items.first;
}
void main() {
print(getFirst<int>([1, 2, 3])); // 1
print(getFirst<String>(["a", "b", "c"])); // a
}
複数型パラメータ
class Pair<K, V> {
final K key;
final V value;
Pair(this.key, this.value);
}
void main() {
var p = Pair<String, int>("age", 20);
print("${p.key}: ${p.value}");
}
ジェネリクス + ミックスイン / インターフェイス
abstract class Repository<T> {
void save(T item);
}
class User {
final String name;
User(this.name);
}
class UserRepository implements Repository<User> {
final List<User> _users = [];
@override
void save(User user) => _users.add(user);
}
ジェネリクスと covariant / invariant の話
Dart のジェネリクスは 原則 invariant(非変性)。
つまり、List<Dog> は List<Animal> のサブタイプではありません。
void feedAnimals(List<Animal> animals) {}
void main() {
List<Dog> dogs = [Dog()];
// feedAnimals(dogs); ❌ エラー
feedAnimals(dogs.cast<Animal>()); // ✅ 安全にキャスト
}
実践例:Repositoryパターン
abstract class BaseRepository<T> {
void insert(T entity);
T? findById(int id);
}
class User {
final int id;
final String name;
User(this.id, this.name);
}
class UserRepository extends BaseRepository<User> {
final List<User> _users = [];
@override
void insert(User user) => _users.add(user);
@override
User? findById(int id) => _users.firstWhere((u) => u.id == id);
}
void main() {
final repo = UserRepository();
repo.insert(User(1, "Anna"));
print(repo.findById(1)?.name); // Anna
}
まとめ
| 概念 | 説明 |
|---|---|
<T> |
型パラメータ |
extends |
型制約 |
T? / List<T>
|
ジェネリクス活用 |
| 関数ジェネリクス | T func<T>(...) |
| 型安全 | 実行前に型ミスを検出できる |