skip to content

What pitfalls arise from nesting apply blocks or referencing the outer scope, and how do labels and this resolution behave?

level: seniorimportance: should knowfreq 48%

answer

  1. Nested apply stacks implicit receivers; inner shadows outer
  2. Bare names bind to nearest matching receiver
  3. this@Label reaches an enclosing receiver
  4. Fix: also (explicit it) or named var
  5. @DslMarker hides outer receivers; plain apply doesn't

basics

~20 s

Inside 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 s

apply 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 lines
kotlin
class 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

for a junior

May not realize nesting stacks receivers; can use a single apply correctly.

for a middle

Understands inner this shadows outer and that bare names hit the nearest receiver.

for a senior

Knows this@Label, uses also/named vars to disambiguate, and recognizes apply lacks @DslMarker protection.

for a principal

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

context