LoginSignup
11
5

More than 3 years have passed since last update.

Kotlin の when で enum を評価するときに、分岐ケースの列挙が不完全であればビルドエラーにする workaround

Posted at

はじめに

Kotlin では when の評価に enum を使うことで、分岐ケースの考慮漏れの検出がしやすくなっています。
具体的には、新たに enum の項目を追加した場合に、コンパイラが追加した項目の分岐ケースの考慮漏れを教えてくれます。

しかしながら、警告だったりビルドエラーだったりまちまちです。。。
この検出を全てビルドエラーとして検出したい! という話です。

その workaround は、 when 式の評価値を受け取ること です。
(歯がゆさが残るので、コンパイラの改善に期待)

警告になるかビルドエラーかの違い

前述の通り、when 式の返値を受け取るか否かで挙動が分かれます。

例)

enum class Kind {
    A, B, C
}

という enum が定義されているときに、

private fun hoge(kind: Kind) {
    when (kind) { // warning...
        Kind.A -> Log.d("", "A")
        Kind.B -> Log.d("", "B")
        // Kind.C が考慮されていない
    }
}

の様に when 式の値を受け取らなければ、警告にしかなりません。
警告が数少なければ検出は容易ですが、警告数が多ければ既存の警告に埋もれてしまい、検出が難しくなります。。。

そこで、

private fun hoge(kind: Kind) {
    val result = when (kind) { // error
        Kind.A -> Log.d("", "A")
        Kind.B -> Log.d("", "B")
        // Kind.C が考慮されていない
    }
}

の様に when 式の値を受け取ると、ビルドエラーとして検出することができるようになります。

しかしながら、単純に val result で受け取ってしまうと、 Variable "result" is never used. の警告を生んでしまいます。。。
( result を使ってないので当然ですね。)

Complete when statement on enumeration にていくつか hack が紹介されていました。

hacks

val _ で受け取る

val _ で受け取ることで、受け取った変数を使わない事を明示して、未使用警告を回避します。

val _ = when (kind) {
    Kind.A -> Log.d("", "A")
    Kind.B -> Log.d("", "B")
    // Kind.C が考慮されていない
}

val _ で受け忘れそう。
無意味な変数に見えるので、不要と判断して消してしまいそう。
コンパイラが対応した際に、workaround を消すのが大変。。。

when の末尾に .let{ } を付ける

when (kind) {
    Kind.A -> Log.d("", "A")
    Kind.B -> Log.d("", "B")
    // Kind.C が考慮されていない
}.let { }

付け忘れそう。
無意味な処理に見えるので、不要と判断して消してしまいそう。
コンパイラが対応した際に、workaround を消すのが大変。。。

when の末尾で自身返す拡張プロパティを使って値を返す

(セクションタイトルが難しい…)

このような拡張プロパティを作成し、

val <T> T.exhaustive: T get() = this
when (kind) {
    Kind.A -> Log.d("", "A")
    Kind.B -> Log.d("", "B")
    // Kind.C が考慮されていない
}.exhaustive

このように使います。

前述の方法と同様、付け忘れそうですが、意味はわかりやすくなったかと思います。
また、workaround を行った箇所はこの拡張関数の利用箇所なので、コンパイラが対応した際にも容易に workaround の削除が可能です。

とはいえ、

123.exhaustive

といったコードが書けてしまう(補完に出てきてしまう)のはまだ微妙な点ですね。。。

なお、この方法は Google I/O 2019 のアプリでも使われていました。
https://github.com/google/iosched/blob/master/shared/src/main/java/com/google/samples/apps/iosched/shared/util/Extensions.kt#L131-L148
(以下に引用します)

/**
 * Helper to force a when statement to assert all options are matched in a when statement.
 *
 * By default, Kotlin doesn't care if all branches are handled in a when statement. However, if you
 * use the when statement as an expression (with a value) it will force all cases to be handled.
 *
 * This helper is to make a lightweight way to say you meant to match all of them.
 *
 * Usage:
 *
 * ```
 * when(sealedObject) {
 *     is OneType -> //
 *     is AnotherType -> //
 * }.checkAllMatched
 */
val <T> T.checkAllMatched: T
    get() = this

やはり、現状はこの workaround が良さそうです。

さいごに

workaround があるとはいえ、埋め込み忘れたら検出できないし、 else -> で丸め込まれたらどうしようもない問題は残っています。
個人的には、 workaround を使わなくてもビルドエラーにできたり、 else -> を禁止したりするコンパイラオプションが増えてくれると嬉しい限りです。
(現状、ビルド警告をエラーにするオプションは存在しますが、警告種別に依らず全てエラーに倒すので使いづらい。。。)

もしくは、Custom lint を作って検出する…かな。

11
5
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
11
5