Kotlin smart cast to non-nullable type and ::class syntax not working together as expected

Viewed 298

I'm trying to understand what's going on with the following code snippet (extracted from a larger project), in which I manipulate a value returned from a Java superclass. The value has type T, unbounded in Java, so it is mapped (I guess) to a platform type T! which I am treating as if it where a T?.

when (val value = node.getConcentration(molecule)) { // it's a Java method returning an unbounded generic T
    is Number -> value.toDouble()
    is String -> value.toDouble()
    is Time -> value.toDouble()
    null -> 0.0 // null has been ruled out, now Kotlin should smart-cast
    else -> throw IllegalStateException(
        "Expected a numeric value in $molecule at node ${node.id}, " +
            "but $value of type ${value::class.simpleName} was found"  // THIS IS THE RELEVANT LINE
    )
}

I would expect this to work, but I get an error on the type lookup: Expression in a class literal has a nullable type 'T', use !! to make the type non-nullable. It looks to me like Kotlin does not understand that since the null case has ben ruled out, the runtime type must be a subclass of Any.

However, it gets stranger:

when (val value = node.getConcentration(molecule)) {
    is Number -> value.toDouble()
    is String -> value.toDouble()
    is Time -> value.toDouble()
    null -> 0.0 // null has been ruled out, now Kotlin should smart-cast
    else -> throw IllegalStateException(
        "Expected a numeric value in $molecule at node ${node.id}, " +
            "but $value of type ${value!!::class.simpleName} was found" // THIS IS THE RELEVANT LINE
    )
}

This compiles, but besides being ugly, this one raises (correctly, IMHO) a warning: Unnecessary non-null assertion (!!) on a non-null receiver of type T -- but this means that the smart casting is work as expected!

I am currently solving as follows:

when (val value = node.getConcentration(molecule)) { 
    is Number -> value.toDouble()
    is String -> value.toDouble()
    is Time -> value.toDouble()
    null -> 0.0
    else -> throw IllegalStateException(
        "Expected a numeric value in $molecule at node ${node.id}, " +
            "but $value of type ${value.let { it::class.simpleName }} was found"
    )
}

This one compiles with no warning, and it is all in all a tolerable amount of boilerplate, but I can't find any reason for value::class.simpleName for raising errors, especially given that if I enforce non-nullability I get an expected warning.

Does anyone understand what's going on here? Is this a bug in Kotlin? I could not find any specific reference to this issue (by the way I'm on 1.4.21).

1 Answers

You can try to change your code like this to separate the null check on the value outside of the when statement, avoid the warnings and also to have cleaner code:

val value = node.getConcentration(molecule)?.let { value ->
    when (value) {
        is Number,
        is String,
        is Time -> value.toDouble()
        else -> throw IllegalStateException(
            "Expected a numeric value in $molecule at node ${node.id}, " +
                "but $value of type ${value} was found"
        )
    }
} ?: 0.0
Related