Dart ジェネリクス

基本配列型 List のAPIドキュメントを確認すると、実際の型は List<E> であることが分かります。<...> という山括弧記号はジェネリック型(パラメータ化型)、つまり仮型パラメータを持つ型を表します。慣例として、ほとんどの型変数は ETSKV のような一文字で命名されます。

ジェネリックを使う理由

  • ジェネリックの核心的な役割は型安全性を保証することです
  • ジェネリックを正しく指定すると、実行効率の良いコードが生成されます;
  • ジェネリックを活用することでコードの重複を大幅に減らせます。

文字列だけを格納するリストにしたい場合は List<String> と宣言します。これが文字列リストです。

文字列専用リストと定義した後、文字列以外の型の値を格納しようとするとエラーになります。

// 静的型チェックによるエラー
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // コンパイルエラー:42 は文字列ではないCode language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // コンパイルエラー:42 は文字列ではないCode language: PHP (php)

コードの冗長化を削減

オブジェクトをキャッシュする抽象インターフェースが必要なケースを考えます:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)

後から文字列のみ保存するキャッシュが必要になった場合、別途インターフェースを新たに作成するしかありません:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Code language: Dart (dart)

その後数値専用のキャッシュも必要になり……ほぼ同じコードを何度も記述することになります。

例えば次のようなコードを作成することになります:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code language: Dart (dart)

ジェネリックを使えば、型パラメータを持つ汎用インターフェースを1つ定義するだけでこの問題を一気に解決できます:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Code language: Dart (dart)

ここの T は型のプレースホルダーで、開発者が後から渡す任意の実体型を指します。

具体的な実例

// ジェネリック抽象キャッシュ基底クラス
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// メモリキャッシュ実装(Mapを基盤)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @Override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @Override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. 文字列キャッシュ例
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. 整数intキャッシュ
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. 独自オブジェクトキャッシュ
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // 削除テスト
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// 独自エンティティクラス
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code language: Dart (dart)

ジェネリックコレクションリテラルの使用

List、Set、Map のリテラルはすべてパラメータ化ジェネリックに対応しています:

  • List / Set:左角括弧の前に <型> を記述
  • Map:左波括弧の前に <キー型, 値型> を記述

例:

// 文字列リスト
var names = <String>['Seth', 'Kathy', 'Lars'];

// 文字列集合(重複自動削除)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// 文字列キー、文字列値の辞書
var pages = <String, String>{
  'index.html': 'ホームページ',
  'robots.txt': 'クローラー向け指示ファイル',
  'humans.txt': '開発者向け説明ファイル',
};
Code language: PHP (php)

実行サンプル

void main() {
  // 1. List<String> 文字列リスト:順序あり、重複許可、インデックスで値取得可
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List リスト ===");
  print(names); // [Seth, Kathy, Lars, Seth] 重複OK
  print(names[0]); // インデックスで先頭要素取得:Seth
  names.add("Mike"); // 末尾に要素追加
  print("追加後:$names");

  // 2. Set<String> 文字列集合:順序なし、自動重複削除、インデックスなし
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set 集合(自動重複削除)===");
  print(uniqueNames); // {Seth, Kathy, Lars} 重複したSethは自動削除
  print(uniqueNames.contains("Kathy")); // 存在確認:true
  uniqueNames.remove("Lars");
  print("Lars削除後:$uniqueNames");

  // 3. Map<String, String> キー値辞書:キーは一意、キーで値取得
  var pages = <String, String>{
    'index.html': 'ホームページ',
    'robots.txt': 'クローラー向け指示ファイル',
    'humans.txt': '開発者向け説明ファイル',
  };
  print("\n=== Map 辞書 ===");
  print(pages);
  print(pages['index.html']); // キーから値取得:ホームページ
  pages["about.html"] = "概要ページ"; // キー値追加
  print("about.html追加後:$pages");

  // 一般的な繰り返し例
  print("\n=== List をループ ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Map の全キー値をループ ===");
  for (var entry in pages.entries) {
    print("ファイル:${entry.key} → 説明:${entry.value}");
  }
}Code language: Dart (dart)

コンストラクタでジェネリックパラメータを使用

コンストラクタを呼び出す際、クラス名後の山括弧内にジェネリック型を指定します:

var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)

下記コードはキーが int、値が View 型の辞書を作成します:

var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("元のリスト:$names");
  print("重複削除後Set:$nameSet");

  // SplayTreeMap には dart:collection のインポートが必要
  var views = SplayTreeMap<int, String>();
  views[5] = "設定ページ";
  views[2] = "ホームページ";
  views[9] = "マイページ";
  views[1] = "起動画面";

  print("\n順序付きTreeMap:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code language: PHP (php)

ジェネリックコレクションの実行時型

型リフィケーション(reified)

Dartのジェネリックはリフィケーション(実体型保持)に対応しています。実行時にジェネリック型情報が完全に保持されます。

実行時にコレクションの具体的なジェネリック型を直接判定できます:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // true を出力
}Code language: PHP (php)

補足比較:Javaのジェネリックは型消去を採用しており、実行時にジェネリックパラメータが削除されます。Javaではオブジェクトが List かどうかの判定しかできず、List<String>List<int> を区別できません。

* ジェネリックパラメータの型制限

境界(bound)について。まだクラスを学習していない場合は概要だけ把握すれば大丈夫です。

ジェネリック定義時、渡す型が特定の親クラスのサブクラスであるよう制限できます。この制約を型境界と呼び、extends キーワードで実装します。

1 非Null型に制限

ジェネリックがNull許可ではない型のみ受け付けるようにする(既定の上限はNull許可の Object?):

class Foo<T extends Object> {
  // 渡すTはNull許可型にできない
}
Code language: JavaScript (javascript)

2 指定した親クラスのサブクラスに制限

extends で基底クラスを指定すると、基底クラスの全メンバメソッドを直接呼び出せます:

class Foo<T extends SomeBaseClass> {
  String toString() => "インスタンス Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)

有効な型引数:親クラス自身、または任意のサブクラス

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)

ジェネリックパラメータを省略した場合、既定で境界型が使用されます:

var foo = Foo();
print(foo); // 出力:インスタンス Foo<SomeBaseClass>
Code language: PHP (php)

関連のない型を渡すと静的コンパイルエラーが発生します:

var foo = Foo<Object>(); // コンパイルエラー:Object は SomeBaseClass のサブクラスではない
Code language: JavaScript (javascript)

* 自己参照型境界(F-bound 有界ジェネリック)

ジェネリックを制約する際、境界に自身の型パラメータを参照させ自己制約を作成でき、これをF-boundと呼びます。

標準例:比較可能インターフェース Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// TはComparable<T>を実装する必要があり、同じ型のオブジェクト同士のみ比較可能
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // 比較ロジック実装
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code language: PHP (php)

T extends Comparable<T> という制約の意味:T は同じ型のインスタンス同士でのみ比較できる。

* ジェネリックメソッド

通常の関数、クラスメソッドもジェネリック型パラメータに対応しています:

T first<T>(List<T> ts) {
  // 事前検証ロジック
  T tmp = ts[0];
  // 後続処理ロジック
  return tmp;
}
Code language: PHP (php)

実例を見てみましょう

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("リストは空にできません");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code language: Dart (dart)

ジェネリックパラメータ <T> は3箇所に作用します:

  1. 関数戻り値型 T
  2. 引数の型 List<T>
  3. ローカル変数の型 T tmp

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です