Reputation: 100408
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?
Upvotes: 49
Views: 6413
Reputation: 344
You can override unary operator to make it less verbose:
operator fun Any.unaryMinus() = Unit
And use it like this:
-when (sealedClass) {
is SealedClass.First -> doSomething()
is SealedClass.Second -> doSomethingElse()
}
Based on this answer
Upvotes: 0
Reputation: 31
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
Upvotes: 1
Reputation: 100408
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'.
Upvotes: 27
Reputation: 46480
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:
hasInfoWithError
to generalize to other i:
s.subprojects { }
or allprojects { }
to apply project-wide.References:
kotlinOptions.allWarningsAsErrors
would solve the issue)Upvotes: 2
Reputation: 14835
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
Upvotes: 11
Reputation: 406
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
}
Upvotes: 27
Reputation: 33789
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
}
Upvotes: 27