A `val` property has a custom getter that may return different values on each access. Why does Kotlin refuse to smart-cast it even though it is declared `val`?
answer
- Custom getter = function call on every read
- Two reads may disagree => no smart cast
- Delegated (by) properties also blocked
- Independent of val/var; fix = read once into local val
basics
~10 sA val with a custom getter is really a function call every time you read it. Two reads can return different values, so checking once does not guarantee the next read is still non-null.
solid answer
~40 sSmart casting requires the compiler to prove that two reads of the same property yield the same value. A plain `val` backed by a field satisfies this. But a `val` with a **custom getter** runs arbitrary code on each access — it may return a fresh or different value every time. So `if (obj.prop != null) { obj.prop.length }` reads `prop` twice, and the second read could be null or a different object. The compiler conservatively refuses the smart cast. The same logic applies to delegated properties (`by`), whose `getValue` is a function call. The fix is to read once into a local `val`: `val p = obj.prop; if (p != null) { p.length }`. Note this is independent of mutability — a `val` getter is non-stable purely because it is computed, not stored.
code
kotlin · 8 linesclass Cache {
val token: String? get() = store["t"] // computed each read
}
fun run(c: Cache) {
val t = c.token // capture once
if (t != null) println(t.length) // smart cast on t, not c.token
}go deeper
Recognizes that a property with a getter behaves like a method call and can apply the capture-into-local fix.
Explains that two reads may differ so the cast is unsound, and knows delegation is affected the same way.
Articulates that the compiler does not analyze getter bodies and treats any custom/open getter as non-stable by rule.
Discusses the soundness tradeoff and why per-getter analysis is intentionally avoided for predictability and modular compilation.
## Stability requires identical reads A smart cast narrows a type only if the compiler can guarantee that the value checked is the *same* value used afterwards. For a property, that means two consecutive reads must return the same thing. ## Backing field vs custom getter A property declared `val x: String? = ...` with no custom accessor is backed by a **field** — reading it just loads that field, and (for a `val`) the field never changes. Stable. A property with a **custom getter** has no such guarantee: ```kotlin class Box { val value: String? get() = if (Random.nextBoolean()) "hi" else null } fun use(b: Box) { if (b.value != null) { // Smart cast NOT allowed: each read of b.value runs the getter again println(b.value.length) // compile error } } ``` Each access to `b.value` invokes `get()`, which is arbitrary code. The check and the use are two *separate* invocations that may disagree. The compiler error reads roughly: *"Smart cast to 'String' is impossible, because 'b.value' is a property that has open or custom getter."* ## Delegated properties too Properties using `by` (delegation) route reads through `getValue`, which is also a function call: ```kotlin val name: String? by lazy { compute() } ``` The compiler cannot prove two reads coincide, so no smart cast. ## This is orthogonal to `val` vs `var` The declaration keyword does not save you: a `val` with a getter is non-stable because it is *computed on each read*, not because it can be reassigned. Conversely, a `private val` field in the same module with no getter is stable. ## The fix: read once into a local `val` ```kotlin fun use(b: Box) { val v = b.value // single read, captured in a stable local val if (v != null) { println(v.length) // smart cast works on v } } ``` ## Why the compiler is strict Allowing the cast would be unsound: if the getter returns null on the second read, `.length` would NPE despite the null check passing. Kotlin's null-safety guarantee depends on this strictness. ## Terms - **Backing field** — the hidden storage (`field`) behind a simple property. - **Custom getter** — user-supplied `get()` that computes the value on each read. - **Delegated property** (`by`) — reads/writes forwarded to a delegate's `getValue`/`setValue`.
- Does this also affect delegated properties like `by lazy`?Yes. Delegation routes reads through `getValue`, a function call, so the compiler cannot prove two reads match. Even `by lazy` (which memoizes) is blocked because the compiler reasons about the general delegate contract, not the specific delegate.
- If I make the getter return a stored field unconditionally, does smart cast return?No. The presence of a custom getter at all blocks the smart cast; the compiler does not analyze the getter body to decide stability.
Asking a custom getter twice is like asking a moody oracle twice — same question, possibly different answer.
saying these in an interview costs you the question
- Believing `val` alone guarantees smart-castability
- Not realizing a custom getter runs on every read
- Thinking the compiler inspects the getter body to allow the cast
- Forgetting delegated (`by`) properties are also affected