Local var nullibility


#1
fun foo(): Bar {
    var maybeNull: Bar? = someNullableFun()
    if (maybeNull == null) {
        maybeNull = Bar("default")
    }
    return maybeNull ?: throw Error("local var changed")
}

Why do I need the explicit null check at the end? Is there a condition I’m missing where maybeNull could be modified?

I know you could write this more nicely:

var maybeNull = someNullableFun() ?: Bar("default")

but in reality I want to do some things in both cases (maybeNull was just created vs maybeNull was not actually null):

var maybeNull: Bar? = someNullableFun()
if (maybeNull == null) {
    Log("some stuff")
    maybeNull = Bar("default")
} else {
    if (maybeNull.someUnacceptableCondition) {
        Log("some other stuff")
        throw Exception()
    }
}
return maybeNull ?: throw Error("local var changed")

I think that structure is more clear than

return someNullableFun()?.also {
    // check stuff
} ?: Bar("default").also {
    // other stuff
}

But maybe not…


#2

You have to handle the null case, because the type of the variable is still nullable outside of the if block. Inside the block is the smart cast from Bar? to Bar effective.


#3

Sure it would work inside the block (as demonstrated by maybeNull.someUnacceptableCondition). But static analysis would tell you that the variable is always assigned a non-nullable type even though it starts out nullable.


#4

I don’t think the compiler is quite as smart as you think it is.

According to the documentation, smart casting for a local nullable variable only works if the variable has not been modified between the nullity check and the point it is used.

In this case the variable is modified after it is checked for nullness and, even though it has been assigned a non-null value, the compiler doesn’t seem to realise this.

Consequently, the variable is still assumed to be nullable outside the ‘if’ statement and you have to deal with it accordingly.


#5

So the compiler is not doing full static analysis? Okay that explains it. Thanks. I do remember reading somewhere that the smart casts are only partially smart. Is there a link to that information?


#6

Which version of Kotlin are you using? I use the latest stable and it tells me the throw is not needed:

afbeelding

If you can’t upgrade, then you can use this:

fun foo() =
        // Note: No "?." before "let"
        someNullableFun().let { bar ->
            if (bar == null) {
                Log("some stuff")
                Bar("default")
            } else {
                if (bar.someUnacceptableCondition) {
                    Log("some other stuff")
                    throw Exception()
                }
                bar
            }
        }

#7

As one of the aims of Kotlin is to compile quickly, I think it’s unlikely that the compiler will ever be able to do a full static analysis of these situations.

Here is the link to what I said in my previous post under the ‘Checking for null in conditions’ section.

However, I made that post without actually trying to compile the code and. following @jstuyts post, I’ve just done that using the current stable version (1.1.3-2) with the same result as he obtained.

So it appears that the compiler has either become smarter since the documentation was written or it doesn’t tell the whole story.


#8

I’m using 1.1.2.

Turns out the issue is that my assignment is using a factory function from Java land and the function is not explicitly annotated for nullability.

My initial example should have been:

fun foo(): Bar {
    var maybeNull: Bar? = someNullableFun()
    if (maybeNull == null) {
        maybeNull = AJavaFactory.defaultBar()
    }
    return maybeNull ?: throw Error("local var changed")
}

Adding a !! to the assignment allows to compiler to do its work.

fun foo(): Bar {
    var maybeNull: Bar? = someNullableFun()
    if (maybeNull == null) {
        maybeNull = AJavaFactory.defaultBar()!!
    }
    return maybeNull
}