Dart ジェネリクス

ジェネリクス(Generics)を使用すると、コードを書くときに具体的な型を指定せず、型パラメータをプレースホルダとして使用し、使用時に型を決定できます。

ジェネリクスの核心的な価値は型安全性とコードの再利用——1つのコードで複数の型に対応し、同時にコンパイラが型エラーをチェックしてくれます。


なぜジェネリクスが必要なのか

まず、ジェネリクスがない場合の課題を見てみましょう。

例

ジェネリクスを使わない書き方:

// ジェネリクスなし:Objectしか格納できず、取り出すときに手動で型変換が必要
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> は型パラメータで、T は「何らかの型」を表すプレースホルダ
// 慣例のな命名: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

複数の型パラメータ

ジェネリクスクラスは複数の型パラメータを持つことができます。

例

// K はキーの型、V は値の型を表す
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)はすべてジェネリクスです。

ジェネリクスコレクションを使用すると、コンパイル時の型チェックが可能になります。

例

void main() {
  // 要素の型を 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)

ジェネリクスリポジトリパターンを使用すると、統一されたインターフェースで異なる型のデータを処理でき、型安全性を保証しつつ、重複コードを回避できます。

その他の拡張