Why !. are required, as the field is never null?

I wonder, why Kotlin compiler insists on placing !! after itemGroup in the following code, at to me it looks obvious that the value of itemGroup is never null:

       val itemViewsList =  mutableListOf<TextView>()
       val viewGroupQueue = java.util.LinkedList<ViewGroup>()
       var itemValue = 0

        // Collect text items using breadth first algorithm
        var itemGroup: ViewGroup? = this
        do {
            for (index in 0 until itemGroup!!.childCount) {
                val item = itemGroup!!.getChildAt(index)
                if (item is ViewGroup)
                    viewGroupQueue.add(item)
                else
                if (item is TextView) {
                    item.setText((++itemValue).toString())
                    itemViewsList.add(item)
                }
            }
            itemGroup = viewGroupQueue.poll()
       } while (itemGroup != null)

The actual reason for the message from the compiler is that you have declared the variable to be of type ViewGroup?. This means that the variable can be null. Whether or not the variable ever actually receives the value null doesn’t actually matter. And while it may be clear to you in this particular case that it’ll never happen, it cannot be proven in the general case, so the compiler relies on the declaration as any other approach would be inconsistent.

1 Like

Actually, OP’s question makes a lot of sense. In Kotlin we have smart casts and they are… well, pretty smart. Example above doesn’t seem like too complicated to be properly supported by smart casts.

I tried to understand why smart casts don’t cover this, so I ran some tests and I’m surprised with results. I don’t know why some trivial cases are not covered by smart casts. For example:

var foo: String? = "foo"
foo.length // error

Here, foo is not smart-casted to String. But here it does:

var foo: String?
foo = "foo"
foo.length // ok

Condition in while is used for smart casts:

var foo: String? = null
while (foo != null) {
    foo.length // ok
}

Also, it seems smart casts can merge from two control flow branches:

var foo : String? = null

if (cond) {
    foo = "foo"
} else {
    foo = "bar"
}

foo.length // ok

But do ... while does not work as expected:

var foo: String?
foo = "foo"
foo.length // ok

do {
    foo.length // error
    foo = null
} while (foo != null)

Code at the start of do ... while block is a merge of the code above the loop and from the code at the end of the loop. In both of these places foo can be smart-casted to String, but for some reason it does not merge similarly to if ... else case.

Either I miss something or smart casts are still pretty new and they don’t support many cases that they really should.

For now you can replace do ... while with while. You will perform one redundant null check, but you won’t need to use !! anymore.

2 Likes

I think this is actually how it is supposed to work in the case of declaration:

var foo: String? = "foo"
foo.length // error

Here you explicitly clear the information that this is not null. The type declaration overrides everything the compiler knows about the type of the value.


var foo: String?
foo = "foo"
foo.length // ok

Here you start with String? then assignment changes the “effective” type from String? to String.


As for the difference between the while and the do...while, it indeed feels like an inconsistency.

I think this is because the actual check might be hard to implement in the compiler. The smart casts are not just about nulls. They are more like type intersections (I guess).

For example:

open class S

class A : S() {
    val a = "A"
}

class B : S()

fun a() {
    var foo: S
    foo = A()

    foo.a // ok
    
    do {
        foo.a // error
        foo = B()
    } while (foo is A)
}

This behaves exactly the same as the null check. If you change the do...while to while it shows no error.

As I see in the case of do...while there are two condition sets the code has: one before the first run of the block and for the subsequent runs. In these examples these are the same, but in some cases they might be in conflict.

Truth to be told, I’m just thinking out loud. :smiley:

1 Like

Yes, I know this is probably what is happening internally. Still, I’m not convinced it should behave like this. I guess for most developers val foo: String = "foo" is just a short way of declaring and initializing a variable and they would be really surprised if I told them this one-liner is not exactly the same as its two-lines alternative.

Besides, I think it is almost never beneficial to not smart cast - it could/should be performed whenever possible.

Yes, they are. In fact, smart casts for nullability and for subtypes are exactly the same, because nullability in Kotlin is represented by the type system. String is a subtype of String?, so smart casting of String? to String is very similar to smart casting S to A.

But I don’t see why this is a problem. We know how to merge two types, we do this all the time both in Kotlin and Java. We just need to find a closest common supertype. Kotlin does this e.g. for type inference. Naive implementation could just merge A + A into A and any other combination would disable smart casting.

I ran some additional tests for types merging and results are even more surprising, at least to me :wink:

fun test1(cond: Boolean) {
    var any: Any
    var a: A

    any = if (cond) C() else D()
    any.b // ok, Any smart casted to B

    a = if (cond) C() else D()
    a.b // error, just A

    val b = if (cond) C() else D()
    a = b
    a.b // ok, A smart casted to B
}

open class A
open class B : A() {
    val b = "b"
}
open class C : B()
open class D : B()

There are two conclusions. First, regular type inference uses more advanced techniques of inferring the type than smart casts. Assigning a value to intermediate variable allowed to guess the type more accurately than when assigning it directly. Regarding the first and second example - I have no idea, it doesn’t make any sense to me that Any was smart casted to B, but A wasn’t.

Regarding do ... while and loops in general I think the problem may be different. If we would like to infer types at the beginning of the loop by utilizing type information at the end of the loop, then we would get into two-way dependency problem. This is definitely solvable, but it is not as trivial as type merging in simple if ... else case.

1 Like

I feel like there’s some situations where it really shouldn’t be performed. For example. let’s say for now you’re using an ArrayList as your “list of choice” inside of your function, but then later on you want to change it to a different List implementation. Now, if the value is auto smart-casted, then suddenly you’ll get missing calls that seemed to originate out of nowhere. I personally feel like whenever a developer specifically adds a type, then they absolutely want that to be the type of the declaration, since Kotlin already has type inference and so they could just remove the specific type.

2 Likes

Hmm, good point. Maybe you are both right. This case I’m talking about (which existed in OP’s example) is actually pretty rare, it happens only if:

  1. We declare a var (not val).
  2. We initially set it to some subtype.
  3. Later, we would like to reassign it to another type.
  4. We would like to recognize the variable initially as the subtype.

In such a specific case it probably makes sense to split declaration and assignment into two lines.

1 Like