【Dart】ジェネリクス型はなぜ必要なのか

こんにちは。エキサイトでアプリエンジニアをしている岡島です。

エキサイトホールディングス Advent Calendar 2024の12日目を担当させていただきます。

tech.excite.co.jp

以前、上記の記事を書いたときにmapの内部処理で用いられているTやEの文字について、これは何かと気になったので調べたことについて共有していこうと思います。

ジェネリクス型とは

ジェネリクス型とは、特定の型に依存せず、柔軟にコードを記述する方法のことです。List や Map<String, int> のようにint型やString型というように型を指定することなく、ジェネリクス型ではListやMap<K, V>というように任意の型の要素を保持できます。

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

ジェネリクス型は、型安全性を保ちながらコードの再利用性を高めるために必要です。それぞれについて詳しく説明していきます。

型安全性の向上

型を指定することで、意図しない型のデータが渡されるのを防ぐ事ができます。

ジェネリクス型を使わない場合

void printItems(List items) {
  for (final item in items) {
    print(item);
  }
}

void main() {
  printItems([1, 2, 3]); // 正常に動作
  printItems(["A", "B", "C"]); // 正常に動作
  printItems([1, "A", true]); // 動作するが、型が混在している
}
  • このコードの問題点
    一見問題ないようにも見えますが、List 内の要素の型が混在しているため、特定の型に依存する処理を行うとエラーが発生する可能性があります。型を定義していないことで、コードの保守性が低くなってしまいます。

ジェネリクス型を使う場合

ジェネリクス型を用いる場合は、型を指定する必要があるので、型安全性が保たれます。意図しない型があるとコンパイルエラーになります。

void printItems<T>(List<T> items) {
  for (T item in items) {
    print(item);
  }
}

void main() {
  printItems<int>([1, 2, 3]); // 型安全に動作
  // printItems<int>(["A", "B", "C"]); // コンパイルエラー
}

型を指定することで、意図しない型のデータが渡されるのを防ぎ、コードの安全性が向上します。

コードの再利用性を高める

ジェネリクス型を用いると、特定の型(例: int や String)に固定されることなく、任意の型に対応することができます。ジェネリクス型を用いて関数を定義した場合、関数を呼び出す際に具体的な型に置き換えられます。

つまり、1つの関数で異なる型に対応できるため、コードの再利用性が高まります。

具体例について見ていきます。

ジェネリクスを使わない場合

こちらの例では、それぞれ異なる型のリストを受け取り、リストの要素を順番にprint出力する関数を定義します。

ジェネリクス型を使わずに実装する場合、扱う型が増えるたびに新しい関数を作成しなければなりません。

void printIntList(List<int> items) {
  for (final item in items) {
    print(item);
  }
}

void printStringList(List<String> items) {
  for (final item in items) {
    print(item);
  }
}

void main() {
  printIntList([1, 2, 3]); 
  printStringList(["A", "B", "C"]); 
}

このコードの問題点

  • 異なる型に対応するためには、新しい関数(printIntList や printStringList)を作成しなければならない
  • リストを出力するロジックが重複している
  • 他の型(例えば、double 型)に対応するには、さらに別の関数を定義する必要がある

ジェネリクスを使う場合

ジェネリクスを使うことで、型に依存しない汎用的な関数を1つだけ書けば、すべての型に対応できます。

void printItems<T>(List<T> items) {
  for (var item in items) {
    print(item);
  }
}

void main() {
  printItems<int>([1, 2, 3]); // int型のリストに対応
  printItems<String>(["A", "B", "C"]); // String型のリストに対応
  printItems<double>([1.1, 2.2, 3.3]); // double型のリストにも対応
}

ジェネリクス型で用いられる名前

ジェネリクス型では、慣例によって以下の文字が用いられるようです。

  • E・・・Element
  • T・・・Type
  • S・・・Second Type
  • K・・・Key
  • V・・・Value

まとめ

ジェネリクスは、コードの柔軟性を高めながらも、型安全性を保つことのできるもので、非常に重要なものだと感じました。個人的に、EやTが何を意味するかが疑問であったため、名前の由来についても知ることができていい機会になりました。

最後まで読んでいただきありがとうございました。