Yup–you and I can guess that it should not be null because we checked the key exists and the method returns something other than null. The compiler doesn’t know that especially since the map doesn’t even promise to return the same thing each call.
You’re asking for the compiler to know that one call to the map
means something else about another call to the map
.
Since there are many implementations of Map
it wouldn’t make sense to assume all of them provide the guarantee of not returning null if the key exists. If we did assume this for all implementations of Map
, the compiler would do invalided smart casts.
You might then ask that Kotlin give special treatment to some specific Map class so that the compiler assumes this guarantee for a specific implementation in the stdlib. This map couldn’t be Map
or ImmutableMap
since both of those are general interfaces–the current implementations weren’t built with the guarantee in mind.
Although this might allow some conveniences for a narrow use-case–instead we really need some mechanism for an author to make promises about their data structure. Or even make promises about properties as well as methods.
For example, as an author, I want clients of my LootBox
data structure to get a smartcast of non-nullable values when the LootBox
contains an item:
val box = LootBox("Sword")
if (box.value == null) exitProcess() // check by directly looking at the value
requireLootInBox(box) // check by some method that exits on no loot (aka, `requireNotNull(map[key])`)
// Here we get a nullable value even though we can assume from our check it should never be null.
val loot: String? = box.value
Just like most maps, it’s pretty reasonable to assume our check of the data structure combined with our assumption of how the data structure works should be enough for a null check.
We need the author to provide some kind of guarantee that these special assumptions are correct. Contracts are how the author provides this special guarantees.
TL:DR
Just because you and I can determine the Map
will probably only return non-null the compiler cannot. One reasonable way to fix this is to allow the author of the data structure to tell the compiler via a contract.
PS
Here are some fun examples that show how you can’t trust non-local (uncaptured values) to be null. What you’re asking for has been asked before but instead of map.get(key)
they wanted it for property getters (which are just methods).
fun main(args: Array<String>) {
//sampleStart
foo = true
if (foo != null) {
println("foo: $foo")
}
//sampleEnd
}
var foo: Boolean? = true
get() = field.also {field = null}
fun main(args: Array<String>) {
//sampleStart
foo = true
if (foo != null) {
println("foo: $foo")
println("foo: $foo")
println("foo: $foo")
}
//sampleEnd
}
var foo: Boolean? = true
get() = when(field) {
true -> { field = false; true }
false -> { field = null; false }
null -> { field = true; null }
}
fun main() {
//sampleStart
foo = true
if (foo == false || foo == null || foo == true) {
// ...
} else {
println("Foo is not true, false, or null! 🤔")
}
//sampleEnd
}
var foo: Boolean? = true
get() = when(field) {
true -> { field = false; true }
false -> { field = null; false }
null -> { field = true; null }
}
fun main(args: Array<String>) {
//sampleStart
if (foo != null) {
doSomething()
print("foo: $foo")
}
//sampleEnd
}
var foo: Boolean? = true
fun doSomething() {
// doSomething ...
foo = null
}