メインコンテンツにスキップ

ジェネリクス

基本的な配列型であるListのAPIドキュメントを見ると、実際にはList<E>という型であることがわかります。<...>という記法は、Listをジェネリック(またはパラメータ化された)型、つまり正式な型パラメータを持つ型としてマークします。慣例として、ほとんどの型変数にはE、T、S、K、Vのような単一の文字名が付けられます。

ジェネリクスを使用する理由

#

ジェネリクスは型安全のために必要とされることが多いですが、コードを実行できるようにする以上の利点があります。

  • ジェネリック型を適切に指定すると、生成されるコードが改善されます。
  • ジェネリクスを使用してコードの重複を減らすことができます。

リストに文字列のみを含めたい場合は、List<String>(「リスト・オブ・ストリング」と読みます)として宣言できます。そうすることで、あなた自身、他のプログラマ、そしてツールは、文字列以外のものをリストに代入することがおそらく間違いであると検出できます。以下に例を示します。

✗ 静的解析: 失敗dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

ジェネリクスを使用するもう1つの理由は、コードの重複を減らすことです。ジェネリクスを使用すると、静的解析の利点を活かしながら、多くの型間で単一のインターフェースと実装を共有できます。たとえば、オブジェクトをキャッシュするためのインターフェースを作成したとします。

dart
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

このインターフェースの文字列専用バージョンが必要であることがわかり、別のインターフェースを作成します。

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

後で、数値専用バージョンが必要だと判断します…お察しの通りです。

ジェネリック型を使用すると、これらのインターフェースをすべて作成する手間を省くことができます。代わりに、型パラメータを受け取る単一のインターフェースを作成できます。

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

このコードでは、Tはプレースホルダー型です。これは、開発者が後で定義する型と考えることができるプレースホルダーです。

コレクションリテラルの使用

#

List、Set、Mapのリテラルはパラメータ化できます。パラメータ化されたリテラルは、これまでに見たリテラルとまったく同じですが、<type>(リストとセットの場合)または<keyType, valueType>(マップの場合)を、開始ブラケットの前に追加します。型指定されたリテラルの使用例を以下に示します。

dart
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines',
};

コンストラクタでのパラメータ化された型の使用

#

コンストラクタを使用する際に1つ以上の型を指定するには、クラス名の直後に山括弧(<...>)で型を囲みます。たとえば、次のようになります。

dart
var nameSet = Set<String>.of(names);

次のコードは、整数キーとView型の値を持つSplayTreeMapを作成します。

dart
var views = SplayTreeMap<int, View>();

ジェネリックコレクションとその含まれる型

#

Dartのジェネリック型はリファイド(reified)であり、実行時に型情報を持つことを意味します。たとえば、コレクションの型をテストできます。

dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

パラメータ化された型の制限

#

ジェネリック型を実装する場合、引数として提供できる型を制限したい場合があります。つまり、引数は特定の型のサブタイプである必要があります。この制限はバウンドと呼ばれます。extendsを使用してこれを行うことができます。

一般的なユースケースとして、型をObject(デフォルトのObject?ではなく)のサブタイプにすることで、null許容でないことを保証します。

dart
class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

Object以外の型でもextendsを使用できます。SomeBaseClassを拡張する例を以下に示します。これにより、SomeBaseClassのメンバーを型Tのオブジェクトに対して呼び出すことができます。

dart
class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {
  ...
}

ジェネリック引数としてSomeBaseClassまたはそのサブタイプのいずれかを使用することは問題ありません。

dart
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

ジェネリック引数を指定しないことも問題ありません。

dart
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

SomeBaseClass以外の型を指定するとエラーになります。

✗ 静的解析: 失敗dart
var foo = Foo<Object>();

自己参照型パラメータの制限(F-bound)

#

バウンドを使用してパラメータ型を制限する場合、バウンド自体を型パラメータに参照できます。これにより自己参照制約、またはF-boundが作成されます。たとえば、次のようになります。

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

int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) => /*...implementation...*/ 0;
}

var useIt = compareAndOffset(A(), A());

F-bound T extends Comparable<T>は、Tがそれ自身と比較可能でなければならないことを意味します。したがって、Aは同じ型の他のインスタンスと比較できるだけです。

ジェネリックメソッドの使用

#

メソッドや関数も型引数を許可します。

dart
T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

ここでは、firstのジェネリック型パラメータ(<T>)により、いくつかの場所で型引数Tを使用できます。

  • 関数の戻り型(T)で。
  • 引数の型(List<T>)で。
  • ローカル変数の型(T tmp)で。