Genericsの一般的な使用例
まず、「そもそもGenericsって何?」という方のために、一般的なGenericsの使用例を確認しておきたいと思います。
Genericsは、『一般的』という意味の英単語(Generic)からも想像できるように、汎用的な型を扱うclassなどを定義したいときに使われるものです。
ちなみに、このGenericsという仕組み・機能は、Kotlinに限らず、多くのプログラミング言語に備わっています。
たとえば、データの型を限定せずに何らかの値をコンストラクタの引数として受け取り、その値の情報を出力するメソッドを備えたclass(GenericsEx)は、次のように定義することができます。
class GenericsEx<T> (private val arg: T) {
fun printArg() {
println(arg)
}
}
fun main() {
val intGenericsEx = GenericsEx<Int>(32)
intGenericsEx.printArg() // Outputs: 32
}
上の例では、整数(Int)を渡していますが、文字列(String)や真偽値(Boolean)などを渡すこともできます。
class GenericsEx<T> (private val arg: T) {
fun printArg() {
println(arg)
}
}
fun main() {
val stringGenericsEx = GenericsEx<String>("hello")
stringGenericsEx.printArg() // Outputs: hello
val booleanGenericsEx = GenericsEx<Boolean>(true)
booleanGenericsEx.printArg() // Outputs: true
}
このように、Genericsを利用して定義することで、データの型を限定しない汎用的なclassを作成することが可能です。
POINT!
- ・Genericsはデータの型を特定せず、汎用的なclassを定義したい場合などに使われる!
- ・Genericsを利用して定義することで、Int,Boolean,Stringなど様々な型のデータを受け入れられる!
似たことはAnyを使ってもできる
前章で、『Genericsは、データの型を限定しない場合に使用する』という説明をしましたが、実はGenerics以外にも、多くのデータ型を受け付けるようにする方法はあります。
その一つが、null以外のデータ型を受け付ける型である Any を利用するという方法です。
前章で紹介したclassとほぼ同じ役割を果たすclassを、Anyを使って表現すると次のようになります。
class AnyEx (private val arg: Any) {
fun printArg() {
println(arg)
}
}
fun main() {
val intAnyEx = AnyEx(16)
intAnyEx.printArg() // Outputs: 16
val stringAnyEx = AnyEx("hi")
stringAnyEx.printArg() // Outputs: hi
val booleanAnyEx = AnyEx(false)
booleanAnyEx.printArg() // Outputs: false
}
Genericsを利用した場合と同じく、様々な型のデータを渡せることが確認できますね。
となると、GenericsとAnyの違いがなんだかよく分からなくなってきませんか?
そこで、classにプロパティを一つ追加して、両者の決定的な違い(の一つ)を明らかにしてみようと思います。
POINT!
- ・Anyを利用することでも、様々なデータの型を受け付けられる!
- ・この時点では、Genericsとの違いが分かりづらいが…!?
Genericsは型情報を保持する
ただ単に、コンストラクタに渡した引数を出力するメソッドを有するだけであれば、GenericsとAnyの違いがよく分かりませんでしたが、コンストラクタに渡した引数をプロパティとして利用することで、その違いを鮮明にできます。
結論から言うと、Genericsではコンストラクタに渡されたデータの型情報を保持するため、データの型に応じて、その型専用のプロパティやメソッドなどをそのまま利用することができます。
たとえば、Genericsとして定義されているコンストラクタのプロパティ(ここではarg)にInt型を渡した場合、argプロパティはその時点でInt型であることが確定し、その後もInt型であるという情報を保持します。
そのため、if文などを用いて条件判定せずとも、Float型に変換する toFloat()
メソッドなどを適用することができます。
以下はその例を示すコードです。
class GenericsEx<T> (private val arg: T) {
fun printArg() {
println(arg)
}
val prop = this.arg
}
fun main() {
val intGenericsEx = GenericsEx<Int>(32)
val floatValue = intGenericsEx.prop.toFloat() // ..1
println(floatValue) // Outputs: 32.0
}
上記のコードの ..1 のラインで、intGenericsEx
インスタンスの prop
プロパティに対し、そのまま toFloat()
メソッドを実行できていることが確認できます。
これは、arg
を介して定義された prop
プロパティがInt型の情報を保持しているからです。
一方、Anyを使った場合、型情報が保持されないため、同じことをしようとするとエラーになります。
class AnyEx (private val arg: Any) {
fun printArg() {
println(arg)
}
val prop = this.arg
}
fun main() {
val intAnyEx = AnyEx(16)
val floatValue = intAnyEx.prop.toFloat() // This code happens an error
println(floatValue) // don't work
}
Anyの場合、条件分岐などを使って型情報をチェックする必要があります。
コードを下のように修正し、条件分岐により prop
がInt型であることを確定させれば、prop
を toFloat()
でFloat型にした結果を得ることができます。
class AnyEx (private val arg: Any) {
fun printArg() {
println(arg)
}
val prop = this.arg
}
fun main() {
val intAnyEx = AnyEx(16)
if(intAnyEx.prop is Int) {
val floatValue = intAnyEx.prop.toFloat() // This is OK
println(floatValue) // Outputs: 16.0
}
}
このように、『型情報を保持するか or しないか』が、GenericsとAnyの重要な違いの一つです。
なので、型情報を保持する必要があったり、型情報が保持された方が都合が良い場合は、Genericsを採用した方が良いと言えます。
一方で、単発のメソッドなど、使い切りで使用されるようなケース(型情報の保持が重要でない場合)では、Anyの方がシンプルで分かりやすいかもしれません。
GenericsとAnyのうち、どちらを使った方が良い…という『正解』が常にあるわけではなく、開発者や開発チームの意図・思想・設計方針などによって左右されるものです。
大切なのは、違いをきちんと理解した上で使い分けることではないかと思います。
POINT!
- ・Genericsでは型情報が保持される!
- ・Anyでは型情報が保持されない!
- ・型専用のプロパティやメソッドをそのまま利用したい場合は、Genericsの方が便利!