[Kotlin]UnitとNothing型の違いをハッキリさせよう

英語では「何も〇〇ない」という否定の意味を持つ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
型の不一致: 推測される型はUnitだが、Stringが要求される

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関数です。

タイトルとURLをコピーしました