I've encountered a situation where Kotlin's nullable type checking is doing the opposite of what I'd expect. Have I misunderstood something fundamental, or is the following not expected behavior?
The return of getOrNull for an Optional of a non-nullable type can be checked for null without the compiler complaining.
class Compiles_TypeNonNullable {
fun foo(o : Optional<String>): String {
val result = o.getOrNull() ?: "fallback"
return result
}
}
While trying to add a null check when the Optional is for a nullable type results in a compile error.
class DoesNotCompile_TypeNullable {
fun foo(o : Optional<String?>): String {
val result = o.getOrNull() ?: "fallback"
return result
}
}
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
[ERROR] public fun <T : Any> Optional<TypeVariable(T)>.getOrNull(): TypeVariable(T)? defined in kotlin.jvm.optionals
This is the precise opposite of what I'd expect.
The above was observed with both Kotlin 1.9.10 and 1.9.23.
getOrNull
only works on Optional<T>
s where T
is non-nullable. This is clear in it's declaration:
public fun <T : Any> Optional<T>.getOrNull(): T? = orElse(null)
The : Any
constraint disallows any nullable types for T
. : Any
isn't technically needed here. The declaration still compiles without it, and it will return the wrapped non-null value or null if the optional is empty. But in Kotlin, Optional<T?>
just doesn't make sense. The type constraint not only makes the intended usage clear, it also discourage people from using Optional<T?>
.
Here is why Optional<String?>
doesn't make sense.
An Optional<T>
either:
T
, or;This makes sense for Optional<String>
- it either contains a string or is empty. getOrNull
returns the string if it is not empty, and returns null otherwise.
According to the same logic, an Optional<String?>
in Kotlin can be any of the following:
The thing is, Optional
cannot represent that second case. Internally, there is just a field of type T
. If this field is null, the optional is empty, otherwise the optional contains an instance of T
. Optional
by design cannot distinguish between the second and third cases.
To represent all 3 cases, you would need a Optional<Optional<String>>
or similar.