问题
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 otheri:
s. - Put this code in
subprojects { }
orallprojects { }
to apply project-wide.
References:
- Issue tracking making missing sealed case as warning:
(which together withkotlinOptions.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