Kotlin when: A switch with Superpowers
There are two kinds of innovation: new perspectives that change how we look at things and pragmatic improvements that change how we do things. Kotlin is full of these pragmatic improvements, getting its user a language that just feels good to use. One of the most useful improvements, especially if you come from Java, is the when
construct.
A traditional switch is basically just a statement that can substitute a series of if/else that make simple checks. However, it cannot replace all sorts of if/else sequences but just those which compare a value with some constant. So, you can only use a switch to perform an action when one specific variable has a certain precise value, like in the following example:
switch(number) { case 0: System.out.println("Invalid number"); break; case 1: System.out.println("Number too low"); break; case 2: System.out.println("Number too low"); break; case 3: System.out.println("Number correct"); break; case 4: System.out.println("Number too high, but acceptable"); break; default: System.out.println("Number too high"); break; }
This is quite limited and while is better than having nothing1, it is useful only in a few circumstances. Instead, the Kotlin’s when
can offer much more than that:
- it can be used as an expression or a statement (i.e., it can return a value or not)
- it has a better and safer design
- it can have arbitrary condition expressions
- it can be used without an argument
Let’s see an example of all of these features.
A Better Design
First of all, when
has a better design. It is more concise and powerful than a traditional switch.
Let’s see the equivalent of the previous switch statement.
when(number) { 0 -> println("Invalid number") 1, 2 -> println("Number too low") 3 -> println("Number correct") 4 -> println("Number too high, but acceptable") else -> println("Number too high") }
Compared to your typical switch
, when
is more concise:
- no complex case/break groups, only the condition followed by
->
- it can group two or more equivalent choices, separating them with a comma
Instead of having a default
branch, when has an else
branch. The else branch is required if when is used as an expression. So, if when returns a value, there must be an else
branch.
var result = when(number) { 0 -> "Invalid number" 1, 2 -> "Number too low" 3 -> "Number correct" 4 -> "Number too high, but acceptable" else -> "Number too high" } // it prints when returned "Number too low" println("when returned \"$result\"")
This is due to the safe approach of Kotlin. This way there are fewer bugs because it can guarantee that it always assigns a proper value.
The only exception to this rule is if the compiler can guarantee that when
always returns a value. So, if the normal branches cover all possible values then there is no need for an else
branch.
val check = true val result = when(check) { true -> println("it's true") false -> println("it's false") }
Given that check has a Boolean
type it can only have two possible values, so the two branches cover all cases: this when
expression is guaranteed to assign a valid value to result.
Arbitrary Condition Branches
The when construct can also have arbitrary conditions, not just simple constants.
For instance, it can have a range as a condition.
var result = when(number) { 0 -> "Invalid number" 1, 2 -> "Number too low" 3 -> "Number correct" in 4..10 -> "Number too high, but acceptable" !in 100..Int.MAX_VALUE -> "Number too high, but solvable" else -> "Number too high" }
This example also shows something important about the behavior of when. If you think about the 5th branch, the one with the negative range check, you will notice something odd: it actually covers all the previous branches, too. That is to say, if a number is 0, is also not between 100 and the maximum value of Int, and obviously the same can be said about 1 or 6, so the branches overlap.
This is an interesting feature, but it can lead to confusion and bugs if you are not aware of it. The ambiguity is solved simply by the order in which the branches are written. The construct when
can have branches that overlap, in case of multiple matches the first branch is chosen. This means that is important to pay attention to the order in which you write the branches: it is not irrelevant, it has meaning and can have consequences.
The range expressions are not the only complex conditions that can be used. The when construct can also use functions, is
expressions, etc. as conditions.
fun isValidType(x: Any) = when(x) { is String -> print("It's a string") specialType(x) -> print("It's an acceptable type") else -> false }
Smart Casts with when
If you use an is
expression, you get a smart cast for free: so you can directly use the value without any further checks. Like in the following example.
when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) }
Remember that the usual rules of smart casts apply: you cannot use smart casts with variable properties, because the compiler cannot guarantee that they were not modified somewhere else in the code. You can use them with normal (unmodifiable) variables.
data class ExampleClass(var x: Any) fun main(args: Array<String>) { var x:Any = "" // variable x is OK when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) } val example = ExampleClass("hello") // variable property example.x is not OK when (example.x) { is Int -> print(example.x + 1) is String -> print(example.x.length + 1) is IntArray -> print(example.sum()) } }
This is the error that you see if you use IntelliJ IDEA and attempt to use smart cast with variable properties.
The Type of a when
Condition
In short, when
is an expressive and powerful construct, that can be used whenever you need to deal with multiple possibilities.
What you cannot do, is use conditions that return incompatible types. You can use a function that accepts any argument, but it must return a type compatible with the type of the argument of the when construct.
For instance, if the argument is of type Int you can use a function that returns an Int, but not a String or a Boolean.
var result = when(number) { 0 -> "Invalid number" // OK: check returns an Int check(number) -> "Valid number" // OK: checkString returns an Int, even though it accepts a String argument checkString(text) -> "Valid number" // ERROR: not valid false -> "Invalid condition" else -> "Number too high" }
In this case, the false condition is an example of an invalid condition, that you cannot use with an argument of type Int. On the other hand, you can use functions that accept any kind or number of arguments, as long as they return an Int
or a type compatible with it.
Among the types compatible with Int, and any other type, there is Nothing
. This is a special type that tells the Kotlin compiler that the execution of the program stops there. You can obviously use it on the right of the condition, inside the code of the branch, but you can also use it as a condition.
var result = when(number) { 0 -> "Invalid number" 1 -> "Number correct" throw IllegalArgumentException("Invalid number") -> "Unreachable code" else -> "Everything is normal" }
Of course, if you do that the exception will be thrown if no previous condition is matched. So, in this example, if number
is not 0 or 1 the code will throw an IllegalArgumentException
.
Using when
Without an Argument
The last interesting feature of when
is that it can be used without an argument. In such case it acts as a nicer if-else chain: the conditions are Boolean expressions. As always, the first branch that matches is chosen. Given that these are boolean expression, it means that the first condition that results True
is chosen.
when { number > 5 -> print("number is higher than five") text == "hello" -> print("number is low, but you can say hello") }
The advantage is that a when expression is cleaner and easier to understand than a chain of if-else statements.
Summary
In this article we have seen how useful and powerful the when
expression is. If you use Kotlin, you will find yourself using the when
expression all the time that you can.
Notes
- Some languages, like Python, do not have a switch statement at all↵
But how can I break inside “when” condition?
I don’t want to have a lot of “if” conditions inside that make the code have a lot of indentation…
For example:
when (something){
someValue->{
val someValue2=foo()
if(someTest2 >3)
break
// do something with someTest2
val someValue2=foo()
if(someTest3 >4)
break
// do something with someTest3
…
}
For this use case there isn’t much special support in Kotlin. To get the cleanest code your best bet might simply be using nested when(s) or to encapsule the code inside each when condition in a function.
Nested “when” is the same as indented “if”. Not sure what you mean by the other solution.
I could use “while(true)” and use the break there, but it would be a bit weird (plus an extra indentation, but at least it’s just once).
You could also use a when without an argument in place of while and have many conditions.
Sorry about the confusing other solution. What I mean is simply that if you don’t like the readability of many ifs inside a when you could hide the code inside functions like this:
when (something) {
someValue -> someValueCase()
someOtherValue -> someOtherValueCase()
}
That’s not always possible, as the conditions can be more sophisticated.
BTW, I also don’t think that Kotlin supports fallthrough, though I really rarely used it on Java, but when I did, it was useful.
The word “when” is temporal in nature and has nothing to do with a conditional. Why did they pick a different name than the perfectly good “switch” keyword and also make such a non-intuitive choice?
I do not know why they pick the word “when”, maybe it is true that it was not the right word. However, I think that it was correct to pick a different word from “switch”, because a traditional switch behaves differently, and if you expect the same behavior from when you will get in trouble. So, if you are new to Kotlin, when should stop you in your track and make you think how it works, instead of assuming it is just a traditional switch.
For instance, with switch you can execute a sequence of branches until you find a break statement, while this is not true for when you always get the first choice. You can use when as an expression, while switch is generally a statement. And so on…
At least when used as an expression, I found “when” to feel more natural than switch, describing the functional behavior, colloquially.
eg.
wakeUpMessage = when (day) {
in Mon..Fri -> currentCommuteTime()
Sat -> movieTheaterSchedule()
else -> inspirationalQuote()
}
I did trip over the temporal aspect of “when” (we’re not wiring up an event handler that could be fired at any time something becomes true), but I haven’t come to anything that felt better.