Force compilation error with sealed classes

删除回忆录丶 提交于 2020-11-25 19:59:27

问题


With sealed classes you can use exhaustive when expressions and omit the else clause when the expression returns a result:

sealed class SealedClass {
  class First : SealedClass()
  class Second : SealedClass()
}

fun test(sealedClass: SealedClass) : String =
    when (sealedClass) {
      is SealedClass.First -> "First"
      is SealedClass.Second -> "Second"
    }

Now if I were to add a Third to SealedClass, the compiler will complain that the when expression in test() is not exhaustive, and I need to add a clause for Third or else.

I am wondering however if this check can also be enforced when test() does not return anything:

fun test(sealedClass: SealedClass) {
    when (sealedClass) {
      is SealedClass.First -> doSomething()
      is SealedClass.Second -> doSomethingElse()
    }
}

This snippet does not break if Third is added. I can add a return statement before when, but this could easily be forgotten and may break if the return type of one of the clauses is not Unit.

How can I make sure I don't forget to add a branch to my when clauses?


回答1:


The way to enforce exhaustive when is to make it an expression by using its value:

sealed class SealedClass {
    class First : SealedClass()
    class Second : SealedClass()
    class Third : SealedClass()
}

fun test(sealedClass: SealedClass) {
    val x = when (sealedClass) {
        is SealedClass.First -> doSomething()
        is SealedClass.Second -> doSomethingElse()
    }  // ERROR here

    // or

    when (sealedClass) {
        is SealedClass.First -> doSomething()
        is SealedClass.Second -> doSomethingElse()
    }.let {}  // ERROR here
}



回答2:


In inspiration by Voddan's answer, you can build a property called safe you can use:

val Any?.safe get() = Unit

To use:

when (sealedClass) {
    is SealedClass.First -> doSomething()
    is SealedClass.Second -> doSomethingElse()
}.safe

I think it provides a clearer message than just appending .let{} or assigning the result to a value.


There is an open issue on the Kotlin tracker which considers to support 'sealed whens'.




回答3:


Our approach avoids to have the function everywhere when auto-completing. With this solution you also have the when return type in compile time so you can continue using functions of the when return type.

Do exhaustive when (sealedClass) {
  is SealedClass.First -> doSomething()
  is SealedClass.Second -> doSomethingElse()
}

You can define this object like so:

object Do {
    inline infix fun<reified T> exhaustive(any: T?) = any
}



回答4:


We can create an extension property on type T with a name that helps explain the purpose

val <T> T.exhaustive: T
    get() = this

and then use it anywhere like

when (sealedClass) {
        is SealedClass.First -> doSomething()
        is SealedClass.Second -> doSomethingElse()
    }.exhaustive

It is readable, shows exactly what it does an will show an error if all cases are not covered. Read more here




回答5:


A discussion triggered me to look for a more general solution and found one, for Gradle builds. It doesn't require changing the source code! The drawback is that compilation may become noisy.

build.gradle.kts

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    val taskOutput = StringBuilder()
    logging.level = LogLevel.INFO
    logging.addStandardOutputListener { taskOutput.append(it) }
    doLast {
        fun CharSequence.hasInfoWithError(): Boolean =
            "'when' expression on sealed classes is recommended to be exhaustive" in this
        if (taskOutput.hasInfoWithError()) {
            throw Exception("kotlinc infos considered as errors found, see compiler output for details.")
        }
    }
}

build.gradle

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
    def taskOutput = new StringBuilder()
    logging.level = LogLevel.INFO
    logging.addStandardOutputListener(new StandardOutputListener() {
        void onOutput(CharSequence text) { taskOutput.append(text) }
    })
    doLast {
        def hasInfoWithError = { CharSequence output ->
            output.contains("'when' expression on sealed classes is recommended to be exhaustive")
        }
        if (hasInfoWithError(taskOutput)) {
            throw new Exception("kotlinc infos considered as errors found, see compiler output for details.")
        }
    }
}

Notes:

  • Change implementation of hasInfoWithError to generalize to other i:s.
  • Put this code in subprojects { } or allprojects { } to apply project-wide.

References:

  • Issue tracking making missing sealed case as warning:
    (which together with kotlinOptions.allWarningsAsErrors would solve the issue)
    https://youtrack.jetbrains.com/issue/KT-37651
  • Discussion triggering this solution:
    https://youtrack.jetbrains.com/issue/KT-12380#focus=streamItem-27-4017839.0-0
  • Repo with working example:
    https://github.com/TWiStErRob/repros/tree/master/kotlin/fail-on-sealed-when-info



回答6:


Consider using the recent library by JakeWharton that allows to just use @Exhaustive annotation.

sealed class RouletteColor {
  object Red : RouletteColor()
  object Black : RouletteColor()
  object Green : RouletteColor()
}

fun printColor(color: RouletteColor) {
  @Exhaustive
  when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }
}

Usage:

buildscript {
  dependencies {
    classpath 'app.cash.exhaustive:exhaustive-gradle:0.1.1'
  }
  repositories {
    mavenCentral()
  }
}

apply plugin: 'org.jetbrains.kotlin.jvm' // or .android or .multiplatform or .js
apply plugin: 'app.cash.exhaustive'

Lib: https://github.com/cashapp/exhaustive



来源:https://stackoverflow.com/questions/38169933/force-compilation-error-with-sealed-classes

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!