LoginSignup
6
3

More than 1 year has passed since last update.

[Kotlin] when式の表現力

Posted at

Javaから派生した言語であるKotlinには、Javaのswitch 〜 case文の後継としてwhen式 (when expression) が用意されています。switch 〜 caseから重要な点が変更されていて単純比較はできないものの、表現力が増して読みやすく書ける場面が増えたといってよいと思います。Rubyのcase 〜 when式などよりも使える表現が多くなっています。

Kotlinではwhenは文でなく式として使えます。つまり値を返させることができます。もちろん、あえて式として使わず文として自由に使うこともできます。

本記事の対象は Kotlin v1.5 以上のバージョンです。本記事のコードは Kotlin v1.5.31 で検証しています。

引数ありwhen式

以下の引数ありwhen式の記述例は、-> の左辺に value に対する条件を、右辺に条件に合致した場合の値(式、もしくは処理)を書いています。

fun printKindOf(value: Number) {
    println(
        when (value) {
            142857 -> "magic number"
            is Double -> "double-precision value"
            !is Int -> "not integer"
            !in -10..10 -> "out of range"
            in -1..1 -> "near origin point"
            2, 3, 5, 7 -> "prime number"
            exp(1.0), sqrt(2.0) -> "typical number"
            else -> "boring number"
        }
    )
}

when式内の一番上の行から順に -> の左辺を評価し、when式の引数と合致する最初の行にて -> の右辺の値が評価され(もしくは処理が行われ)てwhen式が終了します。

関数 printKindOf(Number) は以下のような呼び出しに対して、各行のコメントのような文字列をprintします。

fun testNumbers() {
    printKindOf(142857)       // => magic numbe
    printKindOf(7.0)          // => double-precision value
    printKindOf(1234567890123456789L)  // => not integer
    printKindOf(-142857)      // => out of range
    printKindOf(-1)           // => near origin point
    printKindOf(7)            // => prime number
    printKindOf(4)            // => boring number
}

値との比較

まず、-> の左辺に値を書いて、引数の値と一致したときに右辺の値を返させる(あるいは右辺の処理を実行させる)ことができます。この値はカンマで区切って列挙することで、いずれかの値に一致した場合に右辺を行わせることができます。

        when (value) {
            142857 -> "magic number"
            2, 3, 5, 7 -> "prime number"
            // ...
        }

ここまではJavaでも同じようにできます1が、Kotlinではプリミティブ型・enum型・String型以外の一般の非プリミティブ型との比較も可能です。when式の引数と -> の左辺の値が演算子 ==(equalsインスタンス関数)による比較でtrueを返せば条件成立とみなされます。

    fun printKindOf(intSet: Set<Int>) {
        println(
            when (intSet) {
                TYPICAL_PRIME_SET -> "typical prime set"
                // ...
            }
        )
    }

    companion object {
        private val TYPICAL_PRIME_SET = setOf(2, 3, 5)
    }

    fun testPrimeSet() {
        printKindOf(HashSet<Int>().also { it.add(5); it.add(3); it.add(2) }) // typical prime set
    }

さらに、-> の左辺には定数だけでなく計算式(の列挙)を書くこともできます。これもJavaにはないKotlinの引数ありwhen式の仕様です。

        when (value) {
            exp(1.0), sqrt(2.0) -> "typical number"
            // ...
        }

演算子

Kotlinならではの仕様として、左辺に is, !is, in, !in の4つの演算子が使えます。 is は右側に書いた型に一致するとき条件成立、 !is は右側に書いた型に一致しないとき条件成立となります。in/!in は右側にRangeオブジェクトやコレクションオブジェクトなどが書かれる演算子で、右側の集合が左側の値を「含む」/「含まない」ことを意味します2

is, !is は演算子の多重定義 (Operator overloading) を行えませんが、 in, !in は多重定義が可能です。Operator overloading | Kotlin > in operatorin 演算子を確認すると、

Expression Translated to
a in b b.contains(a)
a !in b !b.contains(a)

となっています。つまり、in/!in 演算子は右側のインスタンスの別名関数 contains() を実行する関数として定義されている、演算子記述と別名関数記述とでインスタンスの並び順が逆になる珍しい演算子です。この独特の文法によって、when式の引数を左側の値とみなして条件式とすることが許容されているようです。

左辺に使える演算子は is, !is, in, !in の4つのみです。たとえば、<, > などを使って大小比較などしてみたいところですが、引数ありwhen式ではできません。in が使えることを利用して、

LessThan.kt
class LessThan<T : Comparable<T>>(val basis: T) {
    operator fun contains(compared: T) = compared < basis
}
GreaterThan.kt
class GreaterThan<T : Comparable<T>>(val basis: T) {
    operator fun contains(compared: T) = compared > basis
}

とin演算子を多重定義したクラスを作っておき、

when (value) {
    // ...
    in LessThan(0) -> "negative boring number"
    in GreaterThan(0) -> "positive boring number"
    // ...
}

のように書くことは可能です。ただ、 in LessThan という書き方がいささか奇異ですので、このようにするよりは引数なしwhen式を使うのがよいでしょう。

引数ありwhen式のelse

引数ありwhen式の最後に条件に else を書くことにより、それまでのいずれの条件も満たさなかったすべての場合を処理させることができます。

引数ありwhen式を式として用い、その値を利用している場合には、所定の例外を除き、 else を書いて、いかなる場合にも値が返されるように(値が未定義とならないように)コードを書かなければなりません。

        println(
            when (intSet) {
                TYPICAL_PRIME_SET -> "typical prime set"
                // else -> "not typical prime set" // Compile error if this line is commented out
            }
        )

所定の例外とは、enum型やBoolean型などで取りうるすべての値を列挙している場合です。この場合は値が未定義とならないことが明らかですので、エラーとはなりません。

fun printKindOf(flag: Boolean?) {
    println(
        when (flag) { // Compile OK
            true -> "TRUUUUUUE"
            false -> "FAAAAAALSE"
            null -> "NUUUUUULL"
        }
    )
}

また、when式の戻り値を参照せず処理だけを書いている場合には、未定義値問題は生じませんので、else なし・列挙しない値あり、でもエラーにはなりません。when式の引数の値に合致する値が列挙されていなければ、その場合は何の処理も行われません。

fun printKindOf(flag: Boolean?) {
    when (flag) {
        true -> println("TRUUUUUUE")
        false -> println("FAAAAAALSE")
        // null -> println("NUUUUUULL") // No operation. compile OK even if this line is commented out
    }
}

else-> の左辺に書かれるので、「任意の条件に合致する条件式」とみなすこともできます。なので is Any? などど常に合致する条件を書いてもよいはずですが、これはwhen式の戻り値を参照している場合にエラーとなりますので、素直に else を使いましょう。

スコープ内変数への同時代入

以下のように、when式の引数の値に変数を割り当ててそれを利用する場合。

@JvmInline
value class NumberHolder(val value: Number)

fun printKindOf(holder: NumberHolder) {
    val number = holder.value
    println(
        when (number) {
            142857 -> "$number: magic number"

            // ...

            else -> "$number: boring number"
        }
    )
    // ...
}

上記の代入文をwhen式の引数の内部に書くことで、ほぼ同じことができます(number の値がwhen式のスコープ内部に限り参照可能になる点のみが、上のコードとは異なります)。

fun printKindOf(holder: NumberHolder) {
    println(
        when (val number = holder.value) {
            142857 -> "$number: magic number"

            // ...

            else -> "$number: boring number"
        }
    )
    // ...
}

これで、定義式の右辺の値をwhen式の引数としつつ、変数 number を使うことができます。KotlinはJavaと違って代入演算子 = が値を持たなくなり、代入値を使う書き方がほとんどの場面でできなくなりましたが、引数ありwhen式では特別に認められています。
ただし、変数の定義に var を使うことはできません。val を使ってください。

引数なしwhen式

when式を引数なしで使うこともできます。引数なしwhen式はもはやswitch 〜 case文の後継ではありませんが、引数ありwhen式より自由な条件を書くことができます。

fun printKindOf(value: Number) {
    println(
        when {
            value == 142857 -> "magic number"
            value is Double -> "double-precision value"
            value !is Int -> "not integer"
            value !in -10..10 -> "out of range"
            value in -1..1 -> "near origin point"
            value == 2 || value == 3 || value == 5 || value == 7 -> "prime number"
            value == exp(1.0) || value == sqrt(2.0) -> "typical number"
            value > 0 -> "positive boring number" // only when expression w/o argument can represent this condition
            else -> "boring number"
        }
    )
}

-> の左辺にはBooleanを返す式を記述します。when式内の一番上の行から順に -> の左辺を評価し、trueのときはwhen式の引数と合致する最初の行にて -> の右辺の値が評価され(もしくは処理が行われ)てwhen式が終了します。

引数なしwhen式のelse

引数なしwhen式のelseも引数ありwhen式のelseと同じく「任意の条件に合致する条件式」として使えます。なので理屈の上では true と書いても同じですが、これもwhen式の戻り値を参照している場合にはエラーとなりますので、必ず else を使いましょう。事実上 else はwhen式の締めくくりに限定される条件文です。

fun printKindOf(value: Number) {
    when {
        value == 142857 -> println("magic number")

        // ...

        // else -> println("boring number") // No operation. compile OK even if this line is commented out
    }
}

引数なしwhen式のelseも、when式の値を参照していなければ省略可能です。

if 〜 else if 〜 else 文との互換性

Javaのswitch 〜 case文が、値を返すwhen式に変わったことで、Kotlinではswitch 〜 case文の「フォールスルー」を表現する条件分岐構文は失われました3。「フォールスルー」は発見しにくい重大なバグの温床となりがちで、その割にあまり使われないことから、Kotlinで採用する価値なしとみなされたのでしょう。

「フォールスルー」のないwhen式はもはや特別な条件分岐を記述するものではなく、またif文もKotlinでif式になりましたので、ifelse ifelse 構文でもまったく同じ条件分岐処理を記述できます。このため IntelliJ IDEA や Android Studio の近年のバージョンではwhen式とif式を相互に変換するリファクタリングのショートカットが用意されています。

冒頭のwhen式をリファクタリングショートカットでif式に書き換えたのが以下のコードです。

fun printKindOf(value: Number) {
    println(
        if (value == 142857) "magic number"
        else if (value is Double) "double-precision value"
        else if (value !is Int) "not integer"
        else if (value !in -10..10) "out of range"
        else if (value in -1..1) "near origin point"
        else if (value == 2 || value == 3 || value == 5 || value == 7) "prime number"
        else if (value == exp(1.0) || value == sqrt(2.0)) "typical number"
        else "boring number"
    )
}

この書き換えから、when式は分岐を上の行から順に判定することがわかります。文法上は、if式は真/偽の2分岐、when式は多分岐のように見えますが、分岐のうち複数の条件を満たす場合はより上の行の条件が必ず優先されます。

if式(if文)を使い慣れたプログラマーにとっては、when式は特別な構文ではなく、「なくてはならないもの」ではないことがわかります。Kotlinのwhen式は、switch文の発展型という以上にBetter if式という位置づけに近いものといえます。なので、if式を使い慣れているからあえてwhen式を多用しない、というプログラミングポリシーでもよいでしょう。

それでも、さまざまな短縮構文を使える、多分岐を読みやすく書ける、という点で、when式を上手に書ければそれは損のないスキルといえると思います。

参考文献


  1. Javaのswitch文では、複数の case ラベルを連続して書くことで、列挙された値のいずれかに合致する場合の処理を書くことができます。 

  2. in演算子は多重定義が可能なので、「含む」/「含まない」という表現がふさわしくないような定義を与えることも可能ですが、混乱の原因となりますので避けましょう。 

  3. 「フォールスルー」がらみで、Javaのswitch文とbreak、Kotlinのwhen式と…という小咄も書いていますので、よろしければこちらもご覧ください。 

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3