Smartcast is impossible because property has open or custom getter
Asked Answered
S

3

63

I am learning Kotlin. My code is as follows:

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    decoupler.attachNotifier(this)
    if(activity is ScreenRouter) {
        decoupler.attachRouter(activity)
    }
}

attachRouter() method:

 fun attachRouter(router: ScreenRouter?) {
    this.router = router
}

As written in documentation, kotlin automatically casts to type after checking with is operator. So, I expected that it would work. But instead it bothers me with compilation error saying :

Smartcast to ScreenRouter is impossible because activity is a property that has open or custom getter.

I thought maybe error is because activity can be nullable so I tried:

if(activity!=null && activity is ScreenRouter) {
     decoupler.attachRouter(activity)
}

But it didn't work and compilation failed with same error.

However, following code works fine:

if(activity is ScreenRouter) {
    decoupler.attachRouter(activity as ScreenRouter)
}

Its okay but above error doesn't seem to explain anything about why smartcast fails. I am not a Kotlin expert, I'm just a beginner learning Kotlin. I found no documentation anywhere. These kind of error descriptions makes Kotlin horrible to learn. Can anyone explain in simple terms?

Somme answered 11/12, 2016 at 12:44 Comment(0)
U
74

The key point here is that an open property or a property with a custom getter is not guaranteed to return the same value on successive calls to it.

Therefore the compiler cannot be sure that, once the value received from the property has been checked, it is safe to assume that it will return the same object or even an object of the same type if called again.

Example (quite simplified and synthetic, though):

open class Base {
    open val value: List<Int> = ArrayList()
}

val b : Base = foo()

fun printArrayList(list: ArrayList<Int>) { /* ... */ }

if (b.value is ArrayList) { // first call
    printArrayList(b.value) // second call, smart cast is impossible
}

This code won't compile, because printArrayList() expects an ArrayList and b.value is open -- that's what you get in your code. Now, let's make up a derived class that demonstrates what could go wrong:

class Derived : Base() {
    private var counter = 0

    override val value: List<Int>
        get() {
            ++counter
            return if (counter % 2 == 0)
                ArrayList() else
                LinkedList()
        }
}

val b = Derived()
println(b.value.javaClass) // class java.util.LinkedList
println(b.value.javaClass) // class java.util.ArrayList

Here it is quite clear that if a property is open, it can be overridden in a way that successive calls to it return different values. In the example with printArrayList() there are two such calls. That's why the smart cast would not be safe. The same is true for properties with custom getters.

Your example which performed an as-cast inside the if block worked because the cast would fail and throw a ClassCastException if the property returned a different value of a non-compatible type on the second call, and this would preserve type safety.

And, on contrary, if a val property is not open and has a default getter that simply returns the value of the backing field (which is final in this case), the compiler can safely perform a smart cast: if you get the value of the property several times it is certain to be the same.


An alternative is to get the value once, store it in a local variable and use it several times instead of using the property again:

val list = b.value

if (list is ArrayList) {
    printArrayList(list) // smart cast to ArrayList
}

Now, no matter if a property is open, there is only one call to its getter, and the code then operates with the value the call returned. Since it cannot change, the smart cast is possible here.

Ulda answered 11/12, 2016 at 13:7 Comment(1)
A more practical example is the use of remember. Anything returned by remember is not "smart cast"able anymore. But this can be solved, by creating a copy of the remembered value. The copied value can be "smart cast"ed.Curmudgeon
P
38

Not related directly to this code snippet, but a common case is if you have a state inside of ViewModel defined as follows:

    private val _state = MutableStateFlow<CardScreenState>(CardScreenState.Loading)
    val state: StateFlow<CardScreenState> = _state

Then when you use this state in a when clause

 when (state) {

You'll get the same error.

Here as the solution you can make a state variable with immutable getter like this:

    when (val screenState = state) {
Pretonic answered 25/11, 2022 at 2:26 Comment(1)
Thank you very much. I am new to Kotlin Flow and StateFlow. This problem had been bugging me for last 2 hours. In my simple app, I was not hoisting the state and I was getting the compile time error. Using your solution fixed the problem for now. Although, I should hoist the state as per official guidelines.Arteaga
N
10

instead of using activity directly which is a nullable object I did this which worked

activity?.let{
   if(it is ScreenRouter) {
      decoupler.attachRouter(it)
   }
}
Nakisha answered 1/3, 2019 at 21:44 Comment(1)
This works, because let creates a copy of the value and stores it in it.Curmudgeon

© 2022 - 2024 — McMap. All rights reserved.