skip to content

Beyond `return@label`, how are labels (`this@label`) used to disambiguate receivers in nested lambdas with receivers, and how does this relate to return labels?

level: seniorimportance: nice to knowfreq 25%

answer

  1. Same labels drive return@ and this@
  2. this@label picks an outer receiver
  3. Inner receiver shadows outer in nested blocks
  4. this@ClassName reaches enclosing instance
  5. @DslMarker restricts implicit outer receivers

basics

~10 s

The same @label names that target return@label also qualify this@label. In nested blocks with receivers (like apply inside apply), this@outerLabel lets you reach an outer receiver instead of the innermost one.

solid answer

~40 s

Kotlin's label grammar serves two purposes that share the same names. `return@forEach` returns from a labelled lambda; `this@forEach` (or `this@ClassName`) selects which **receiver** you mean when several are in scope. In lambdas with receivers — `apply`, `run`, `with`, or any `T.() -> R` — the receiver is the implicit `this`. When such lambdas nest, the innermost `this` shadows the outer ones. The implicit label (function name) or an explicit `name@` lets you write `this@apply` / `this@outer` to reach the enclosing receiver, and `this@MyClass` reaches the surrounding class instance. So return labels and qualified-`this` are two faces of the same labeling mechanism: one controls control flow, the other resolves the receiver. This is central to writing type-safe Kotlin DSLs, often combined with `@DslMarker` to restrict implicit outer-receiver access.

code

kotlin · 12 lines
kotlin
class Outer { val tag = "outer" }
class Inner { val tag = "inner" }

fun Outer.build(block: Outer.() -> Unit) = block()
fun Inner.use(block: Inner.() -> Unit) = block()

Outer().build outer@ {
    Inner().use {
        println(this.tag)          // inner (nearest receiver)
        println(this@outer.tag)    // outer (qualified this via label)
    }
}

go deeper

for a junior

May only know return@label; unaware that labels also qualify this.

for a middle

Understands this@ClassName to reach the enclosing instance from a lambda.

for a senior

Resolves nested receiver shadowing with this@label and ties it to return labels.

for a principal

Designs type-safe DSLs using receiver labels plus @DslMarker to control scope access deliberately.

## One label grammar, two uses Kotlin reuses the `@label` name in two places: - **`return@label`** — non-/local return targeting a specific lambda. - **`this@label`** — a *qualified this* that picks one receiver when several are in scope. Both draw on the **implicit label** (the called function's name) or an **explicit `name@`** label. ## Lambdas with receivers A *lambda with receiver* has type `T.() -> R`; inside it, the receiver `T` is the implicit `this`. Scope functions `apply`, `run`, and `with` use this form: ```kotlin class Builder { var name = ""; val tags = mutableListOf<String>() } Builder().apply { name = "root" // 'this' is the Builder this.tags.add(name) } ``` ## Nesting shadows the receiver When receiver lambdas nest, the **innermost `this` shadows** outer receivers: ```kotlin class Html { fun body(block: Body.() -> Unit) { Body().block() } } class Body { fun p(block: P.() -> Unit) {} ; val bodyId = "b" } class P { val pId = "p" } Html().body bodyLabel@ { p { // 'this' here is P; to reach Body use the outer label: val outerId = [email protected] } } ``` Without a label, `this` is the nearest receiver (`P`). `this@bodyLabel` (explicit) or `this@body` (implicit, function name) reaches the enclosing `Body`. ## Reaching the surrounding class Inside any lambda you can reach the enclosing class instance with `this@ClassName`: ```kotlin class Service { val id = 1 fun run() = listOf(1).forEach { println([email protected]) // the Service instance } } ``` ## Relationship to return labels The mechanisms are parallel: - `return@body` → control flow: leave the `body` lambda. - `this@body` → data access: use the `body` lambda's receiver. Both are resolved by the same label, so an explicit `name@` simultaneously names the return target and the receiver qualifier. ## DSLs and `@DslMarker` This receiver resolution underpins type-safe builder DSLs. `@DslMarker` annotations restrict accidental access to *outer* implicit receivers, forcing an explicit `this@outer` when you genuinely need the enclosing scope — improving safety in nested DSLs. ## Key terms - **Lambda with receiver**: `T.() -> R`; `this` is the receiver. - **Qualified this `this@label`**: selects a specific receiver/class instance. - **Shadowing**: inner receiver hides outer receivers. - **`@DslMarker`**: restricts implicit outer-receiver access in DSLs.

  • If `return@forEach` and `this@forEach` use the same label, how do you keep two nested receivers straight?
    Give each lambda an explicit label (`outer@`, `inner@`); then `this@outer`/`this@inner` and `return@outer`/`return@inner` unambiguously select the receiver or return target you mean.
  • What does `@DslMarker` add on top of qualified `this`?
    It makes implicit access to an outer DSL receiver a compile error, so you must explicitly write `this@outer` to reach it — preventing accidental cross-scope calls in nested builders.

saying these in an interview costs you the question

  • Thinking labels only apply to returns, not to `this`
  • Believing the outer receiver is reachable implicitly when shadowed
  • Confusing `this@ClassName` with `this::class`
  • Not knowing nested receiver lambdas shadow `this`
  • Unaware `@DslMarker` interacts with implicit receiver access

context