What is the order of execution of branches in a “when” block?
The documentation says the execution is in order (link); however, I stumbled on this comment that says it is not always executed sequentially. If the execution is not in order, it would be nice to update the documentation to say so.
Consider the function below:
fun foo(newObject: SomeObject, existingObject: SomeObject) {
when {
newObject.createdAt == null -> { /* do something */ }
existingObject.createdAt == null -> { /* do something */ }
newObject.createdAt.isAfter(existingObject.createdAt) -> { /* newObject.createdAt and existingObject.createdAt should not be null */ }
else -> { /* do something */ }
}
}
A NullPointerException can be thrown at the third branch if the execution is not sequential.
I think that comment is misleading slightly. What it’s referring to, I think, is limited situations where some operation (like an is check) is known to be false, and hence it can be skipped. Of course, any side effects are still forced to run.
I’d definitely trust the documentation on this vs an old comment from 2016 (which was made right about the time when Kotiln 1.0 was released; we’re at 2.3 now!)
Regardless, if the Kotlin compiler smart casts something, you can trust it! If it turns out to be wrong, that’s a bug that should be reported (unless you have an incorrect contract, which is on you)
Thanks for the response. I do trust the kotlin compiler (most times, however this was flagged by spotbugs during static analysis. I would assume then that this is a false positive and proceed accordingly.
To add to my response, this isn’t even about side effects. The semantics of when guarantee that the first feasible branch will be taken (otherwise,when behaviour would be “probabilistic”, or dependent on compiler optimizations).
So, regardless of any type of “we may reorder when condition execution” (which, again, I’m pretty sure only happens in the case of constant conditions, including when an is check is known to be true/false, and definitely not in a way that would skip a side effect), the priority of the branches still applies
when guarantees it uses the first condition that matches. But conceptually, it is not a sequential code, but more like a map from conditions to effects. Compiler could decide to implement it one way or another. Please consider this example:
It could be internally implemented as a chain of if elses, but it is much more efficient to implement it as:
private val names = arrayOf("zero", "one", "two", "three")
fun toEnglishName(n: Int) = names[n]
(Let’s ignore the “else” case)
With this implementation when still fulfills its contract to pickup the first condition that matches, but technically there is no sense of sequential execution of checks anymore. Suggested improvement in the linked discussion isn’t really applicable here.
To add to that, I’m rather sure that the compiler guarantees any side effects in the conditions to be executed sequentially, so you can always pretend as if the conditions are indeed executed sequentially until one results in true, it’s just that some conditions that have no side effects (e.g. equality with a known integer) might be optimized out
I think if the compiler isn’t warning you about possible null values, then I would trust the compiler. If you DO get an NPE, then like @kyay10 said, that’s a bug in the compiler that should be reported to JetBrains.
This is not necessarily a false positive. If SomeObject.createdAt is a var, then it can change from non-null to null between those when condition checks, so Spotbugs could well be right in pointing out a possible NPE.
Even if those fields are vals, it may be possible that they change between the checks. Remember: val means read-only for you.
If SomeObject is in the same module, the compiler can check that they can never change. Otherwise the compiler must assume the value could possibly change.
So if you move SomeObject from the module with the above code to another module, a compilation error may (actually should) appear.