Explain why an `open val` property on a class cannot be smart-cast, and how this relates to overriding.
answer
- open => could be overridden by a getter
- Dynamic dispatch hides the real accessor at compile time
- final field-backed val (same module) IS smart-castable
- abstract val never smart-castable; fix = local val
basics
~20 sAn open val can be overridden by a subclass, and the override might use a custom getter that returns different values each read. So the compiler cannot assume reads are stable and blocks the smart cast.
solid answer
~40 sAn `open` property may be overridden in a subclass, and the override can replace a simple backing-field `val` with a **custom getter** that returns different values on each access. Because the compiler resolves an `open` member's actual implementation only at runtime (dynamic dispatch), it cannot prove that two reads of an `open val` return the same value. So it conservatively refuses the smart cast, exactly as it does for an explicit custom getter. This holds even if the base declaration is a plain field-backed `val`: the *potential* for an overriding getter is enough. The fix is the usual one — read once into a local `val`. The same applies to `abstract val` (no backing field at all) and protected/open members accessed polymorphically.
code
kotlin · 7 linesopen class Node { open val next: Node? = null }
fun walk(n: Node) {
// val capture needed: n.next is open, not smart-castable
val nx = n.next
if (nx != null) walk(nx)
}go deeper
Knows open properties can't be smart-cast and applies the local-val workaround.
Explains overriding can introduce a custom getter, making reads non-stable, and that final field-backed vals are fine.
Ties it to dynamic dispatch and separate compilation: the real accessor is unknown statically, so worst-case assumption applies.
Connects to modular compilation guarantees and why the rule must be subtype-agnostic to remain sound across separately compiled modules.
## `open` means overridable In Kotlin, classes and their members are `final` by default. Marking a property `open` permits subclasses to override it: ```kotlin open class Base { open val value: String? = "x" // field-backed in Base } class Sub : Base() { override val value: String? get() = if (Random.nextBoolean()) null else "y" // custom getter! } ``` ## Dynamic dispatch defeats the proof When you hold a `Base` reference, a read of `value` dispatches to whichever override the runtime object actually has. The compiler cannot know statically that it is the field-backed `Base.value` versus `Sub`'s recomputing getter. So it must assume the worst — a getter that can change between reads: ```kotlin fun use(b: Base) { if (b.value != null) { println(b.value.length) // compile error: open property, no smart cast } } ``` The error message names this directly: the property is `open` and may have an overriding accessor. ## Same family of limitation This is the same root cause as custom getters: **non-stable reads**. The compiler treats a value as stable only when it can prove every read yields the same result. `open` properties, custom getters, delegated (`by`) properties, and properties from other modules (where the compiler can't see the implementation) all fail that proof. ## `abstract val` is even clearer An `abstract val` has no backing field and *must* be implemented by a subclass, typically via a getter. It is never smart-castable. ## The fix Read once into a local `val`, then operate on the local: ```kotlin fun use(b: Base) { val v = b.value if (v != null) println(v.length) // smart cast on the stable local } ``` ## Terms - `open` — keyword allowing a class/member to be subclassed/overridden. - `final` — default in Kotlin; cannot be overridden (a `final val` backed by a field *is* smart-castable in the same module). - **Dynamic dispatch** — selecting the actual method/getter implementation at runtime based on the object's real type. - `abstract val` — declared without implementation; subclasses must provide it.
- If I mark the same property `final` (the default), is it smart-castable?If it's a field-backed `final val` in the same module with no custom getter, yes. Removing `open` removes the possibility of an overriding getter, restoring stability.
- Why doesn't the compiler just check whether the override has a getter?It can't in general: the actual subtype is only known at runtime via dynamic dispatch, and subclasses may exist in other modules compiled separately. So it must assume the worst case.
An open val is a job description a subclass can rewrite — you can't trust how the work gets done until runtime.
saying these in an interview costs you the question
- Saying `open val` is smart-castable because the base uses a field
- Ignoring dynamic dispatch / polymorphism in the reasoning
- Confusing `open` with mutability (var)
- Not knowing `abstract val` is never smart-castable