What pitfalls arise from nesting apply blocks or referencing the outer scope, and how do labels and this resolution behave?
answer
- Nested apply stacks implicit receivers; inner shadows outer
- Bare names bind to nearest matching receiver
- this@Label reaches an enclosing receiver
- Fix: also (explicit it) or named var
- @DslMarker hides outer receivers; plain apply doesn't
basics
~20 sInside nested apply blocks, 'this' refers to the innermost object, which can shadow the outer one. You use labeled this, like this@Outer, to reach an enclosing receiver, and be careful that unqualified names resolve to the nearest scope.
solid answer
~50 sapply changes this to the receiver, so nesting two apply blocks makes the inner this shadow the outer one. To reference an enclosing receiver you use a qualified this with a label: this@Outer or this@ClassName. Unqualified property/method names resolve to the *nearest* receiver that has a matching member, which can silently bind to the wrong object — a real bug when inner and outer share member names. apply's receiver is also implicit, so reviewers can't easily see which object a bare name targets; deep nesting hurts readability. Mitigations: avoid deep apply nesting, switch the inner block to also so the object is an explicit it, use labeled this for clarity, or extract a named variable. The @DslMarker annotation solves this structurally for real DSL builders by making outer implicit receivers inaccessible, but plain apply is not annotated, so it offers no such protection.
code
kotlin · 9 linesclass Node { var name: String = ""; val children = mutableListOf<Node>() }
val tree = Node().apply { // this = root
name = "root"
val child = Node().apply { // this = child; shadows root
name = "child" // sets child.name, NOT root.name
}
children.add(child) // children resolves to root.children
}go deeper
May not realize nesting stacks receivers; can use a single apply correctly.
Understands inner this shadows outer and that bare names hit the nearest receiver.
Knows this@Label, uses also/named vars to disambiguate, and recognizes apply lacks @DslMarker protection.
Sets guidance on when to graduate from apply to a @DslMarker DSL and how to make builder scopes safe by construction.
## How this resolves inside apply `apply`'s block is `T.() -> Unit`: the object becomes the *receiver*, so inside the block `this` is that object and bare names resolve against it. When you nest `apply` blocks, you stack receivers: ```kotlin outer.apply { // this = outer inner.apply { // this = inner; outer is now an *outer implicit receiver* name = "x" // resolves to inner.name if inner has 'name' } } ``` The innermost receiver wins for unqualified access. If both `outer` and `inner` have a `name` member, the bare `name` binds to **inner**, silently — a classic source of bugs. ## Reaching the outer receiver with labeled this A qualified `this@Label` selects a specific enclosing receiver. The label is the class name for a class receiver, or for scope-function lambdas you can name the receiver via the implicit function label: ```kotlin class Outer { val tag = "outer" fun build(inner: Inner) = inner.apply { this.value = 1 // inner.value [email protected](this) // reach the enclosing Outer instance } } ``` For reaching the *outer apply's* receiver specifically, the cleanest fix is usually to not rely on labels at all but to make scopes explicit. ## Resolution rules to remember - Unqualified names resolve to the **nearest** receiver (or enclosing scope) that declares a matching member. - An outer receiver is still reachable *implicitly* as long as the inner one has no conflicting member — which means refactors that add a member to the inner type can silently change resolution. - `this` alone always means the innermost receiver. ## Mitigations 1. **Don't nest apply deeply.** Two or three implicit receivers is already hard to read. 2. **Use also for the inner object** so it's an explicit `it` and every access is qualified: ```kotlin outer.apply { inner.also { i -> i.name = "x" // unambiguous name = "y" // clearly outer.name } } ``` 3. **Extract named variables** when configuration is non-trivial. 4. **Use labeled `this@Type`** to disambiguate when you must keep nesting. ## @DslMarker — the structural fix for real DSLs For purpose-built type-safe builders, the `@DslMarker` annotation marks DSL receiver types so that, within nested builder lambdas, **outer implicit receivers become inaccessible** — you must qualify them explicitly. This prevents accidentally calling an outer builder's method from an inner block. Plain `apply` is *not* a DSL-marked receiver, so it gives **no** such protection; this is one reason to graduate from `apply` to a real `@DslMarker`-annotated builder once configuration gets nested and error-prone. ## Bottom line `apply` is great for a single object's configuration. Nesting it stacks implicit receivers and invites silent mis-binding; prefer explicit `it` (via `also`), labeled `this`, or a proper `@DslMarker` DSL once depth grows.
- Why does @DslMarker matter for nested builders but not for a single apply?@DslMarker makes outer implicit receivers inaccessible inside nested DSL lambdas, preventing accidental cross-scope calls. A single apply has only one receiver, so there's nothing to shadow or protect against.
- How would you make an inner configuration block unambiguous without labels?Use also instead of apply for the inner object so it's exposed as an explicit it, or bind it to a named val and configure that variable directly.
Like two people both named 'this' in a room: shout an unqualified name and the nearest one answers — you need a surname (this@Outer) to address the other.
saying these in an interview costs you the question
- Claiming nested apply blocks can't shadow because each has its own this (the shadowing is the problem)
- Thinking plain apply gets @DslMarker scope protection automatically
- Believing unqualified names always bind to the outermost receiver
- Not knowing this@Label syntax exists
- Advising deep apply nesting as good style