Smart cast to combine different objects with the same fields (or the same inherited class)

Take this, for example:

private fun updatePreference(preference: Preference?) {
    val stringValue: CharSequence
    when (preference) {
        is SwitchPreferenceCompat, is SwitchPreference -> {
            stringValue = preference.switchTextOn
        }
    }
    ...
}

Both SwitchPreference and SwitchPreferenceCompat have a switchTextOn property, yet that gives the error “Unresolved reference: switchTextOn”

However, the following does not have said issue:

private fun updatePreference(preference: Preference?) {
    val stringValue: CharSequence
    when (preference) {
        is SwitchPreferenceCompat -> {
            stringValue = preference.switchTextOn
        }
        is SwitchPreference -> {
            stringValue = preference.switchTextOn
        }
    }
    ...
}

What appears to happen is that it falls back to Preference? in the former example and does a smart cast in the latter.
I believe it makes sense to a person familiar with the above classes that preference.switchTextOn in the code in the former example will resolve to a defined value.

I’m not sure as to the scope of implementation for something like this, but this would certainly be very handy in many other situations. Even if the functionality was such that it was only able to smart cast to TwoStatePreference, which is extended by both the above classes, that would still be incredibly useful.

In that case, why don’t you do:

 when (preference) {
      is TwoStatePreference -> {
          stringValue = preference.switchTextOn
      }

}

Because switchTextOn is only available in a SwitchPreference / SwitchPreferenceCompat. I used this as a contrived example for that very reason.

Something such as isChecked would be available in TwoStatePreference, however.

 when (preference) {
      is TwoStatePreference -> {
          // this is fine
          stringValue = preference.isChecked

          // However, this will fail
          stringValue = preference.switchTextOn
      }
}

If all we wanted was isChecked then that would work, however what if there was another class that extended TwoStatePreference and we didn’t want that to be included?

Here’s a better example:

open class A
open class B: A() {val arbitraryValue: Any? = null}
class C: B() {...}
class D: B() {...}
class E: B() {...}
class F {
    fun check(a: A) {
        when (a) {
            is C, is D -> {
                // This will fail
                a.arbitraryValue
            }
            is E -> {
                // This is fine
                a.arbitraryValue
            }
        }
    }
}

I understand your problem. My remark was in direct response to what I quoted.

hi bashenk.

when you write when (preference) { is SwitchPreferenceCompat, is SwitchPreference -> {...your condition will be match if preference is a SwitchPreferenceCompat or a SwitchPreference. then the compiler cannot smartcast to any of these types because any of these type are candidate. So there is no smartcast here.

with comment in your code :

private fun updatePreference(preference: Preference?) {
    val stringValue: CharSequence
    when (preference) {
        is SwitchPreferenceCompat, is SwitchPreference -> {
            /*
here compiler does not know if its a SwitchPreferenceCompat or a SwitchPreference.
kotlin will not assume anything and stick to the original type (Preference?).
there is no smartcast
            */
            stringValue = preference.switchTextOn //error
        }
    }
    ...
}

private fun updatePreference(preference: Preference?) {
    val stringValue: CharSequence
    when (preference) {
        is SwitchPreferenceCompat -> {
/*
here the compiler knows preference is not only a Preference?,
but also a SwitchPreferenceCompat
kotlin can then consider that preference is a SwitchPreferenceCompat:
there is a "smartcast" in this case
so now kotlin knows preference is a SwitchPreferenceCompat, it can access switchTextOn property
*/
            stringValue = preference.switchTextOn
        }
        is SwitchPreference -> {
// same case here, with SwitchPreference instead of SwitchPreferenceCompat
            stringValue = preference.switchTextOn
        }
    }
    ...
}

if SwitchPreferenceCompat and SwitchPreference are part of your code, you can add add an interface on them and use it to access switchTextOn:

interface WithSwitchTextOn{
  val switchOnText: CharSequence
}

class SwitchPreferenceCompat ... : WithSwitchTextOn {
  ...
  override val switchOnText: CharSequence
  ...
}
class SwitchPreference ... : WithSwitchTextOn {
  ...
  override val switchOnText: CharSequence
  ...
}

then you can write :

private fun updatePreference(preference: Preference?) {
    var stringValue: CharSequence
    when (preference) {
        is WithSwitchOnText -> {
            stringValue = preference.switchTextOn
        }
    }
    ...
}

if you don’t have control on these 2 classes… you will have to treat them separatly (like in your second example).

btw, you stringValue is not initialized, I guess this is just for the example so its ok :stuck_out_tongue_winking_eye:

1 Like

For the most part I was aware of the compiler’s decision rationale. I was attempting to make a suggestion, but apparently that didn’t come across fully.

That said, I like your suggestion for using an interface.

So I’ll see about using that when I have the opportunity to do so.

Yeah yeah, haha :upside_down_face:
I was trying to minimize what I used in the example for legibility.

While it’s great that you like the solution of using an interface, what you actually want (and simulate this way) are union types. I hope I did not mean intersection types. I always get them mixed up. I know there were a few discussions about them previously. While I am personally not a fan of declaring union types explicitly I think they might have some value in combination with smart casts (is-expressions).
Not sure if there has been any real discussion about this.

1 Like

Yes, union type would solve the problem.
To remembers difference between ‘union type’ and ‘intersection type’ when you come from Java,

union type is used in Java with multiCach :

 catch (IOException | SQLException ex)  // ex is of type IOException Or SQLException)

intersection type is used in Java with generic :

class MyClass<T extends Serializable & Cloneable> {} // T implements Serializable And Cloneable.

To respond to the original question:

Both SwitchPreference and SwitchPreferenceCompat have a switchTextOn property

Yes, but those are two different properties. They just happen to have the same name.

So the compiler can’t tell which of the two properties you want to set, hence the error.

If you made the two classes implement the same interface, they’d then have the same property, which is would works OK.

(But I don’t see how a union type would solve this, as that still wouldn’t resolve the ambiguity between properties.)

1 Like