Last time I wrote about another issue regarding version 3.6.1 of the Android Gradle Plugin. This week, I have some information to share about when
statements in Kotlin.
The when
statement in Kotlin is analogous to the switch
statement in other C-like languages. However, it’s been given a few additional capabilities.
With a switch
statement, one can only use it to check against primitive types (such as integers), Strings, or enums. In Kotlin, the when
statement can also be used with sealed classes.
One of the benefits of a sealed class is that it has additional features on top of an enum, but with the same property that all options are defined at compile-time.
On top of this, Kotlin supports a feature known as exhaustive when expressions
, where if your when
statement does not list all of the possible options (or use an else
branch), you will get a compiler error.
From the Kotlin documentation:
If when
is used as an expression, the else
branch is mandatory, unless the compiler can prove that all possible cases are covered with branch conditions (as, for example, with enum class
entries and sealed class
subtypes).
This is a really useful feature in that it allows you to force clients to handle new cases that might be added in the future, potentially preventing future bugs from occurring.
However, it’s important to note that not all uses of the when
operator are exhaustive. For example, the following when
statement will not have its completeness enforced by the compiler:
val x = 5
when(x) {
1 -> doSomething()
5 -> doSomethingElse()
}
The reason for this is that the result of the when
is not used by anyone. However, if you instead do the following, the compiler will enforce the completeness:
val x = 5
val result = when(x) {
1 -> doSomething()
5 -> doSomethingElse()
}
//☝️ Won't compile, because there may be other possible integer values you haven't accounted for!
Personally, I find it a little irritating that when
statements aren’t exhaustive in all cases. And, I think it can be a little cumbersome to have to assign the output of the when
statement to a variable when you don’t need to use it — this would cause a lint warning if you don’t use result
, and someone else may later come along and take Android Studio’s suggestion to remove result
, which will also remove the exhaustive-ness from this statement!
I did some internet searching, and discussed this with my team, and we came up with a few options to help solve this problem:
Option A
// Definition
val Any?.exhaustive get() = Unit
// Usage
when (foo) { ... }.exhaustive
Pros: really clean syntax, clear, easy to use!
Cons: since exhaustive
is declared as an extension function on the Any
type, it will be globally suggested as an option to apply to all types by Android Studio, not just when
statements.
Option B
// Definition
object Do {
inline infix fun<reified T> exhaustive(any: T?) = any
}
// Usage
Do exhaustive when(foo) { ... }
Pros: syntax is still pretty clear, doesn’t pollute the global namespace!
Cons: a little more verbose than Option A
Option C
// Definition
object Exhaustive {
operator fun invoke(any: Any?) = any
}
// Usage
Exhaustive(
when(foo) { ... }
)
Pros: shorter syntax than Option B, and you get to use the fun operator invoke
trick!
Cons: the extra required parenthesis make this a little clunkier (believe me, I tried everything I could think of to get rid of them)
Option D
val action = when(sealedClass) {
is sealedClass.A -> ::doSomething
is sealedClass.B -> ::doSomethingElse
}
action()
Pros: No funky definitions required, and this is exhaustive!
Cons: Does not work if you need to pass parameters to your functions
Option E
val sealedClass = someMethodThatReturnsASealedClass()
handleSealedClassOptions(sealedClass)
fun handleSealedClassOptions(sealedClass: SealedClass) =
when(sealedClass) {
is sealedClass.A -> doSomething(sealedClass)
is sealedClass.B -> doSomethingElse(sealedClass)
}
Pros: Very clean syntax, makes surrounding code easier to read
Cons: May have to define a unique method for each call site
Summary
It’s important to note that there’s not necessarily a *best* option here. Each one has its advantages. Personally, I will probably be using Option E in my app, as I like how it will allow me to force exhaustiveness for all uses of a particular type of Sealed Class, and in the places where I’m intending to use this (RxJava subscribe()
blocks), will be much easier to read.
Note that from talking to the rest of my team, some people feel that in some cases forcing a when
to be exhaustive may be a bad practice, and you should follow the language’s setup and only have when
statements be exhaustive in cases where the language supports it (you’re doing something with the resulting value of the when
statement). So, it’s really one of those things where it just depends on your unique situation/preferences.
I hope you learned something useful about when
statements! If you have other solutions to this problem, or want to share your preferences/opinion on this topic, leave a comment below! And, please follow me on Medium if you’re interested in being notified of future tidbits.
Interested in joining the awesome team here at Intrepid? We’re hiring!
This tidbit was originally delivered on March 27, 2020.
Top comments (0)