🧵

【Dart】安全で効率的なコードを書くためのGenerics(総称型)基礎

2020/12/21に公開

【Dart】Generics(総称型)

はじめに

  • 【参考】公式サイト

  • Generics(総称型)とは

    • 利用目的
      • 特定の型を記載せず総称型を仮置きし、後で決定する
    • メリット
      • 重複したコードを無くせる
      • 違う型の変数に代入すると、コンパイルエラーで教えてくれる
  • 実行環境

    • DartPadやAndroid Studio等で実行
      • Dart SDK 2.10.4

Genericsを使わない場合

  • String変数を持つUserクラス
    • イメージ:お店や病院の呼び出し名が名前(文字列)
class User {
  final String callName;
  User(this.callName);
}

void main() {
  final user = User('Tanaka');
  print(user.callName);
}

実行結果

Tanaka
  • int変数を持つUserクラス
    • 上述のコードが先に存在する場合は、クラス変更や、名前を変えて複数作成する必要がある
    • イメージ:お店や病院の呼び出し名が番号(数字)
class User {
  final int callName;
  User(this.callName);
}

void main() {
  final user = User(1);
  print(user.callName);
}

実行結果

1
  • dynamic変数を持つUserクラス
    • dynamic callNameとすると、文字列を持てる
      • final callNameと記載すると、callNameはdynamic型となる
class User {
  final callName;
  User(this.callName);
}

void main() {
  final user = User('Tanaka');
  print(user.callName);
}

実行結果

Tanaka
  • dynamic変数を持つUserクラス
    • dynamic callNameとすると、同様に数字も持てる
class User {
  final callName;
  User(this.callName);
}

void main() {
  final user = User(1);
  print(user.callName);
}

実行結果

1
  • dynamic変数は便利だが、実行時エラーでしか気づけないバグを作ってしまう可能性がある
    • エラーとなる例
      • dynamic callNameだと、intで値を作成した後に、Stringの変数に代入しようとするとコンパイルエラーとならずに、実行時エラーとなる
      • エラーとしないために以下等が可能だが、忘れると実行時エラーとなるため危険
        • String name = user.callName.toString();
class User {
  final callName;
  User(this.callName);
}

void main() {
  final user = User(1);
  print(user.callName);
  String name = user.callName;
}

実行結果

1
Uncaught Error: TypeError: 1: type 'JSInt' is not a subtype of type 'String'

Genericsを使う場合

  • Genericsを持つUserクラス
    • コンパイルエラーとなる(IDEが教えてくれる)
      • String name = user.callName;で以下エラー
        • A value of type 'int' can't be assigned to a variable of type 'String'
class User<T> {
  final T callName;
  User(this.callName);
}

void main() {
  final user = User(1);
  print(user.callName);
  String name = user.callName;
}

実行結果

Error compiling to JavaScript:
main.dart:9:22:
Error: A value of type 'int' can't be assigned to a variable of type 'String'.
  String name = user.callName;
                     ^
Error: Compilation failed.
  • Genericsを持つUserクラス
    • T callNameとすると、文字列を持てる
class User<T> {
  final T callName;
  User(this.callName);
}

void main() {
  final user = User('Tanaka');
  print(user.callName);
}

実行結果

Tanaka
  • Genericsを持つUserクラス
    • T callNameとすると、同様に数字も持てる
    • クラスを書き換えることなく、安全に複数の型が利用可能
class User<T> {
  final T callName;
  User(this.callName);
}

void main() {
  final user = User(1);
  print(user.callName);
}

実行結果

1
  • 【備考】<T><TEST>等何でもよい
    • 慣習的に主にTを利用
class User<Test> {
  final Test callName;
  User(this.callName);
}

void main() {
  final user = User('Tanaka');
  print(user.callName);
}

実行結果

Tanaka
  • 明示的に型を記載
    • User<String>('Tanaka')User<int>(1)とすることで、Genericを利用していることが分かりやすくなる
class User<T> {
  final T callName;
  User(this.callName);
}

void main() {
  final user = User<String>('Tanaka');
  print(user.callName);
}

実行結果

Tanaka

Genericsの階層

  • Genericsを持つクラスを継承
    • 注意
      • スーパークラスのGenericsの名前と合わせる
        • class SingleUser<A> extends User<T> {}だと以下エラー
          • The name 'T' isn't a type so it can't be used as a type argument
      • スーパークラスの指定では、後ろの<T>まで必要
        • extends User<T>ではなく、extends Userとすると、user1.callNameがdynamic型になってしまう
          =違う型の変数に代入しても、コンパイルエラーで教えてくれない
abstract class User<T> {
  final T callName;
  User(this.callName);

  void call();
}

class SingleUser<T> extends User<T> {
  SingleUser(T callName) : super(callName);

  void call() {
    print('call SingleUser');
    print(callName);
  }
}

class MultiUser<T, K> extends User<T> {
  final K callNamePlus;
  MultiUser(T callName, this.callNamePlus) : super(callName);

  void call() {
    print('call MultiUser');
    print(callName);
    print(callNamePlus);
  }
}

void main() {
  final user1 = SingleUser<String>('Tanaka');
  user1.call();
  final user2 = MultiUser<int, String>(1, 'Tanaka');
  user2.call();
}

実行結果

call SingleUser
Tanaka
call MultiUser
1
Tanaka
abstract class User<T extends num> {
  final T callName;
  User(this.callName);

  void call();
}

class SingleUser<T extends num> extends User<T> {
  SingleUser(T callName) : super(callName);

  void call() {
    print('call SingleUser');
    print(callName);
  }
}

class MultiUser<T extends num, K> extends User<T> {
  final K callNamePlus;
  MultiUser(T callName, this.callNamePlus) : super(callName);

  void call() {
    print('call MultiUser');
    print(callName);
    print(callNamePlus);
  }
}

void main() {
  final user1 = SingleUser<int>(1);
  user1.call();
  final user2 = MultiUser<int, String>(2, 'Tanaka');
  user2.call();
}

実行結果

call SingleUser
1
call MultiUser
2
Tanaka

Generics関数

  • T(TEST等でもよいが、慣習的にTやK)を戻り値とした書き方
    • Tを引数とし、Tを返す関数
      • T myself<T>(T name) => name;
    • Tを引数とし、何か処理
      • void check<T>(T val) {}
    • 備考
abstract class User<T extends num> {
  final T callName;
  User(this.callName);

  // Generics methods
  T myself<T>(T name) => name;
  
  void check<T>(T val) {
    print('型名は以下');
    print(T);
    print(val);
  }
}

class MultiUser<T extends num, K> extends User<T> {
  final K callNamePlus;
  MultiUser(T callName, this.callNamePlus) : super(callName);
}

void main() {
  final user = MultiUser<int, String>(2, 'Tanaka');
  final int userNameInt = user.myself<int>(user.callName);
  final String userNameString = user.myself<String>(user.callNamePlus);
  print(userNameInt);
  print(userNameString);
  user.check<int>(user.callName);
  user.check<String>(user.callNamePlus);
}

実行結果

2
Tanaka
型名は以下
int
2
型名は以下
String
Tanaka

Discussion