目次

ジェネリクス

基本的な配列型である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は代替型です。これは、開発者が後で定義する型と考えることができるプレースホルダーです。

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

#

リスト、セット、マップのリテラルはパラメータ化できます。パラメータ化されたリテラルは、既に見てきたリテラルと変わりませんが、開始ブラケットの前に<>(リストとセットの場合)または<キー型値型>(マップの場合)を追加する点が異なります。型付きリテラルの使用例を次に示します。

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>.from(names);

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

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

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

#

Dartのジェネリック型は*具象化*されます。つまり、実行時に型情報が保持されます。たとえば、コレクションの型をテストできます。

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>();

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

#

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

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をいくつかの場所で se使用できます。

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