英語では「何も〇〇ない」という否定の意味を持つnothingですが、KotlinのNothing型は少々特別な型です。今回は
- Nothing型とは何か
- Nothing型とAny型の関係
- Unitとの違いとNothing型の使い道
についてお話します。
Nothing型とは何か
Nothing型は関数の戻り値としてしばしば用いられる1つの型ですが、「戻り値が無い」という意味ではありません。Kotlinで戻り値が無ければ「Unit」を指定するか、そもそも何も書かなければ「戻り値無し」の関数として成立しています。
ではこのNothing型とは一体何なのか。
Nothing型とAny型
答えを一言で言うなら、Nothing型は全てのクラスを継承したサブクラスです。
この対極にあるのがAny型。Any型とnull許容型のAny?型は全てのクラスのスーパークラス(継承元)です。そのため型指定でAny
を指定すると、null以外であれば型を問わず変数に代入することができます。
val lst: List<Any> = listOf(0,"",listOf(1..3),setOf(4,5),JvmType.Object("x"), compareValues(1,2))
Nothing型はちょうどこの逆。どんな値もNothing型の変数には代入できません。例外はnull許容型である「Nothing?型」にnullを代入することだけです。
言い方を変えれば「究極に抽象化したクラス」がAny型、「究極に具体化したクラス」がNothing型です。例えばあなたが作ったAnimalクラスやPersonクラスはAny型を継承し、Nothing型に継承されています。
だから何?
究極に具体化するとどうなるでしょう?その型は全てのクラスからメソッドを引き継ぐため、理論上は全てのクラスのメソッドを持つことになります。
しかし問題は、その小さすぎる器に入る値が無いことです。そのためNothing型はインスタンスを生成することができません。
つまりNothing型を戻り値の型として指定された関数は、インスタンスを返すことは絶対に無いということです。戻り値が無いのではありません。インスタンスが無いのです。
Unitとの違いをコードで見る
ここが「Unit」(戻り値無し)との決定的な違いです。まずはUnitを返す関数を見てみましょう。
fun main() {
println(example(3))
}
fun example(i: Int): String{ //example関数の定義
if(i >= 1){
return "OK"
}else{
return fail() //fail関数ではUnitが返される
}
}
fun fail(): Unit{ //fail関数の定義
throw Exception()
}
fail関数にUnitを指定したこのコードはエラー。
//Type mismatch: inferred type is Unit but String was expected
example関数はInt型を引数に取り、引数の値によって異なるString型を返す関数ですが、内部で呼び出されるfail関数の戻り値はUnitです。このためexample関数の戻り値はString型とは言い切れず、コンパイルエラーになります。
しかしfail関数の戻り値にNothing型を指定すると、
fun main() {
println(example(3))
}
fun example(i: Int): String{
if(i >= 1){
return "OK"
}else{
return fail()
}
}
fun fail(): Nothing{
throw Exception()
}
//OK
コードは問題無く動きます。これはNothingがStringを継承したサブクラスだからです。
インスタンスを返さず、nullにもなり得ないというNothing型の特徴を活かし、例外を送出するような関数をnull許容型を使わずに実装することができます。
TODO関数
型の性質が分かったところで、Kotlin標準ライブラリで用意された「Nothing型を返す関数」をご紹介しましょう。TODO関数です。
fun TODO(): Nothing
fun TODO(reason: String): Nothing
定義はごくシンプルですが、これは何に使えるのでしょう?一度実行してみます。
TODO()
//kotlin.NotImplementedError: An operation is not implemented.
「NotImplementedError」が送出されます。これはその名の通り、まだ実装されていないことを示す例外エラーです。例えばこういった場合に使用します。
fun main(){
something()
}
fun something(): String {
TODO("まだ考え中")
val x = "something"
}
//...An operation is not implemented: まだ考え中
something関数はStringを返すということは決定していますが、今のところ未完成です。この関数が値を返すことはありません。
それをコンパイルエラー無しでチームのメンバー(あるいは未来の自分)に知らせるには、TODO関数で例外エラーをメッセージと共に送出するのも1つの方法です。
TODO関数は呼び出しに対して必ず例外エラーを送出することを保証し、それでいて結果がnullになることは無く、それ以降のコードを実行させないためコンパイルエラーにはなりません。「まだここでやることが残っている」のを知らせるのがTODO関数です。