What problems arise from nesting receiver-based scope functions (apply/run/with), and how do this/it choices and labels help you avoid them?
answer
- Nested apply/run = multiple implicit this
- inner this shadows outer this
- fragile: adding a member flips resolution
- fix: use it + rename, or this@label
- @DslMarker guards outer-receiver access in DSLs
basics
~20 sNesting blocks that all use 'this' makes it unclear which object a bare property refers to, and inner this hides outer this. Prefer 'it'-based functions when nesting, name the parameter, or qualify this with a label.
solid answer
~40 sWhen you nest apply/run/with, each opens a new implicit this receiver. A bare member call resolves to the nearest receiver that has it, so the inner this shadows the outer — a property added to the inner type later can silently change resolution, and identical member names become ambiguous to readers. Fixes: (1) switch the inner (or outer) scope to an it-based function (let/also) and rename the parameter (also { node -> }) so each object has a distinct name; (2) use a qualified this with a label — this@Outer or this@apply — to disambiguate; (3) avoid deep nesting entirely by extracting a function. The receiver model is powerful for DSLs (with @DslMarker restricting outer-receiver access) but error-prone in ad-hoc nesting.
code
kotlin · 18 linesclass Form { var title = "" }
class Field { var title = "" }
// Ambiguous with nested apply:
Form().apply {
Field().apply {
title = "field" // hits Field.title (inner shadows)
this@apply.title = "form" // qualified label reaches outer
}
}
// Clear with also + names:
Form().also { form ->
Field().also { field ->
field.title = "field"
form.title = "form"
}
}go deeper
Recognizes that bare names refer to the object inside an apply/run block.
Knows inner this shadows outer this and can rename via it/also to fix it.
Explains resolution order, the refactoring fragility, and uses this@label labels correctly.
Connects the receiver model to DSL design and @DslMarker, and sets guidelines limiting receiver nesting in code review.
## The implicit-receiver model Receiver-based scope functions (`apply`, `run`, `with`) take a *function literal with receiver* `T.() -> R`. Inside, the object is the **implicit receiver** `this`, so bare names like `name` mean `this.name`. When you nest these blocks, you create **multiple implicit receivers** in scope simultaneously. ## Problem 1 — receiver shadowing Name resolution for a bare member walks from the **innermost** receiver outward and stops at the first one that declares it. So the inner `this` shadows the outer: ```kotlin class Outer { var status = "outer" } class Inner { var status = "inner" } Outer().apply { // this = Outer Inner().apply { // this = Inner (shadows Outer) status = "changed" // sets Inner.status — NOT Outer.status! } } ``` The bare `status` silently targets `Inner`. Worse, this is **fragile**: if `Inner` *didn't* have `status` today it would resolve to `Outer`, but adding a `status` to `Inner` later flips the meaning with no compile error — a refactoring hazard. ## Problem 2 — readability / ambiguity A reader can't tell which object a bare member belongs to without knowing every receiver type. Deeply nested `apply`/`run` blocks become opaque. ## Fix A — prefer `it` and rename when nesting Use `let`/`also` and give each object a distinct name: ```kotlin Outer().also { outer -> Inner().also { inner -> inner.status = "inner changed" outer.status = "outer changed" // unambiguous } } ``` Explicit names remove all ambiguity. This is the most common practical fix. ## Fix B — qualified `this` with labels Every receiver block introduces an implicit label (the function name) and class scopes have `this@ClassName`. Disambiguate explicitly: ```kotlin Outer().apply { // implicit label this@apply (Outer) Inner().apply { this.status = "inner" // inner Inner [email protected] = "outer" // outer Outer's apply receiver } } ``` Within a class method, `this@Outer` reaches the enclosing class instance over a scope-function receiver. ## Fix C — don't nest; extract The cleanest fix is often to pull the inner configuration into a named function so there's only one receiver per scope. ## Where receivers are intentionally good: DSLs and @DslMarker The same multi-receiver model powers type-safe builders (e.g. Kotlin's `buildString`, HTML DSLs). To prevent accidental access to an **outer** receiver from an inner DSL block, Kotlin offers the `@DslMarker` annotation: annotate the DSL's receiver types, and the compiler forbids calling an outer receiver's members implicitly from an inner block, forcing a qualified `this@Outer`. This turns the shadowing footgun into a compile-time guardrail. So: receivers are excellent for designed DSLs, risky for ad-hoc nested scope functions.
- Why is shadowing especially dangerous during later refactors?A bare member resolving to an outer receiver can silently re-bind to an inner one if that inner type gains a member with the same name — no compile error, changed behavior.
- How does @DslMarker prevent this in DSLs?It makes implicit calls to an outer marked receiver a compile error inside an inner block, forcing an explicit this@Outer and eliminating accidental outer access.
Nested 'this' blocks are like two people both named 'this' in the room — say 'this@Outer' or give them real names (it) to avoid talking to the wrong one.
saying these in an interview costs you the question
- Claiming bare members always resolve to the outermost receiver
- Not knowing this@label exists
- Deeply nesting apply/run without disambiguation
- Unaware of @DslMarker's role
- Thinking it-based functions have the same shadowing issue (they use named parameters)