How is smart cast different from explicit cast in KOTLIN

Viewed 1377

Recently I read about the smart cast performed by the is operator and also about the as or the better as?operators in that is used for explicit casting.

The kotlin docs species their difference of usage as follows:-

Note that smart casts do not work when the compiler cannot guarantee that the variable cannot change between the check and the usage. More specifically, smart casts are applicable according to the following rules:

  • val local variables - always except for local delegated properties;

  • val properties - if the property is private or internal or the check is performed in the same module where the property is declared. Smart casts aren't applicable to open properties or properties that have custom getters;

  • 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;

  • var properties - never (because the variable can be modified at any time by other code).

Note that smart casts do not work when the compiler cannot guarantee that the variable cannot change between the check and the usage.

The above written is bit confusing, since the var variables can be changed after initialization and I could not find a example that can make it clear the actual insight of the statement.

Can anyone make it easier to understand this insight better, in anyway?

And does is operator provide some optimization benefit over the as operator if, any?

1 Answers

The idea of a smart cast is to help you avoid using as or as? to explicitly cast something that was already checked. As far as the bullet points above, here are some examples.

  • val local variables - since a val is final (cannot be changed), after you do the check, the variable can be smart cast as it cannot change again.
val a: Int? = 2
if (a is Int) {
    // 'a' is smart cast to Int
    val b = a * 2 // b is also Int
}
  • val properties - if accessed directly (via the default getter), smart cast is possible. If through a custom getter, it's not because we cannot know it was modified.
class Test {
    val a: Int? = 2;
}

class TestGetter {
    val a: Int? = 2
        get() = field * 2
}

// Usage
val test = Test()
val testGetter = TestGetter()

if (test.a is Int) {
    // 'test.a' is smart cast to Int
    val b = test.a * 2
}

if (testGetter.a is Int) {
    // smart cast is impossible, 'testGetter.a' is still Int?
    val b = testGetter.a * 2 // can't happen because we don't know whether 'a' was changed by the custom getter or not (the getter could give unstable values)
}
  • 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;
var a: Int? = 2
if (a is Int) {
    // 'a' was not changed, so it can be smart cast to Int
    val b = a * 2 // b is also Int
}

var c = 4
if (c is Int) {
    c = null
    // 'c' was changed between the check and the usage, we cannot smart cast it anymore
    val b = c * 2 // won't work
}

var properties - the var can always be modified by something else in the code, so smart cast won't work.

class Example {
    var a: Int? = 2

    fun test1() {
        if (a is Int) {
            // smart cast is impossible because we don't know whether 'a' was changed by some other code/function
            val b = a * 2 // won't work
        }
    }
}

As far as using as goes, if you look at the last example:

class Example {
    var a: Int? = 2

    fun test1() {
        if (a is Int) {
            // smart cast is impossible because we don't know whether 'a' was changed by some other code/function
            val b = a as Int * 2 // this WILL work because we forcefully cast it to Int, but if a is null there will be an exception in runtime
        }
    }
}

You can also use as? when you're not sure if the var can be cast to something or not. If not, it will just give you a null. For example:

val a: Double = 2.0
val b = a as? String // 'b' will be 'String?', in this case initialized to 'null' since 'a' cannot be cast to it

val c: Int? = 2
val d = c as? Int // 'd' will be '2' but still 'Int?' since 'as?' always makes the variable nullable

Hope the examples helped, let me know if I need to clarify something further.

Related