Search code examples
kotlintype-inferencekotlin-null-safety

Kotlin type inferrence failed


This kotlin code:

fun badKotlin(text: String?): Boolean {
    if (text == null) {
        return true
    }

    var temp = text
    if (false) {
        temp = Arrays.deepToString(arrayOf(text))
    } 

    return temp.isBlank() // <-- only safe (?.) or non null asserted (!!.) calls
}

does not compile with message: only safe (?.) or non null asserted (!!.) calls are allowed on a nullable receiver of type String?

But if I add else:

fun badKotlin(text: String?): Boolean {
    if (text == null) {
        return true
    }

    var temp = text
    if (false) {
        temp = Arrays.deepToString(arrayOf(text))
    } else {
        temp = Arrays.deepToString(arrayOf(text))
    }

    return temp.isBlank()
}

all compiled. So, why type inferrence failed?

If I change type of temp to var temp: String = text it is successfully copmiled! So, moreover, If we change assignment of temp like this: temp = String.format("%s", text) it is compiled too.

UPDATE:

Successfully copmpiled:

fun badKotlin(text: String?): Boolean {
    if (text == null) {
        return true
    }

    var temp = text
    if (false) {
        temp = String.format("%s", text)
    } 

    return temp.isBlank() // <-- only safe (?.) or non null asserted (!!.) calls
}

And this:

fun badKotlin(text: String?): Boolean {
    if (text == null) {
        return true
    }

    var temp: String = text
    if (false) {
        temp = Arrays.deepToString(arrayOf(text))
    } 

    return temp.isBlank() // <-- only safe (?.) or non null asserted (!!.) calls
}

Solution

  • You might be thinking that after

    if (text == null) {
        return true
    }
    

    the type of text is refined to String instead of String?.

    But it seems like it isn't; instead the compiler inserts a smart cast when it sees text is used where a String is required. In the

    var temp = text
    

    line there is no reason to insert a cast, so the compiler doesn't, and the type of temp is String?.

    If you write

    var temp: String = text
    

    the cast is necessary and so the compiler does insert it.

    If you write

    if (...) {
        temp = Arrays.deepToString(arrayOf(text))
    } else {
        temp = Arrays.deepToString(arrayOf(text))
    }
    

    the compiler sees that whatever happens, temp has been assigned a value of platform type String! which again can be smart-cast to String. Without a else branch, this doesn't happen.

    EDIT:

    Curiously, if you just remove if and leave

    fun badKotlin(text: String?): Boolean {
        if (text == null) {
            return true
        }
    
        var temp = text
        
        return temp.isBlank()
    }
    

    it does compile, and if my explanation was complete I wouldn't expect it to. So the compiler does maintain information needed for the smart cast, but it appears not to get applied because

    More specifically, smart casts are applicable according to the following rules: ...

    • var local variables - if the variable is not modified between the check and the usage, is not captured in a lambda that modifies it, and is not a local delegated property;

    In the if-else case, the assignments in two branches together serve as another check; in the if-only, the one branch doesn't.