Given multiple implicit receivers plus extension functions in scope, walk through Kotlin's priority order for resolving an unqualified call. Where do member functions of inner vs outer receivers and imported extensions sit?
answer
- Proximity first: inner receiver beats outer
- Per level: members then extensions
- Inner extension > outer member (proximity wins)
- Member > extension at the same receiver
- Override with this@Label; guard with @DslMarker
basics
~20 sKotlin tries the closest receiver first, considering both its members and extensions on it, before moving to outer receivers. A nearer receiver — member or applicable extension — beats anything reachable only through a farther receiver.
solid answer
~50 sResolution is driven by the implicit-receiver scope stack, searched innermost to outermost. For each receiver level, the compiler gathers candidates: the receiver's own member functions and any extension functions in scope that apply to that receiver's type. The nearest receiver that yields an applicable candidate wins, and that prevents farther receivers from contributing. Within a single receiver, an applicable member generally takes priority over an extension on the same type. Extensions become available via imports or being defined in the current scope, and an extension on an inner receiver still outranks a member of an outer receiver because the inner scope is searched first. The overall ordering is therefore: by receiver proximity first (inner > outer), then member-over-extension within a level. To override proximity you qualify with this@Label; @DslMarker can forbid implicit selection of outer same-marker receivers entirely.
code
kotlin · 13 linesclass Inner
class Outer { fun tag() = "outer-member" }
fun Inner.tag() = "inner-extension"
fun outer(b: Outer.() -> Unit) = Outer().b()
fun Outer.inner(b: Inner.() -> Unit) = Inner().b()
outer {
inner {
tag() // "inner-extension" (Inner level searched first)
this@outer.tag() // "outer-member" (forced to outer receiver)
}
}go deeper
May only know that the closest receiver is tried first.
Can state proximity-first and member-over-extension within a level.
Reasons through the combined order, including inner-extension-beats-outer-member, and the scope requirement for extensions.
Designs DSL APIs to avoid hostile collisions, choosing @DslMarker scoping and naming so resolution is predictable and self-documenting.
## The scope chain for an unqualified call When you write a bare `foo()`, the compiler resolves it against an ordered chain of scopes. For DSL/builder code the dominant axis is the **implicit-receiver stack**, searched **innermost → outermost**. At each receiver level it forms candidate sets, and the **first level that produces an applicable candidate wins** — farther levels don't get a vote. ## What counts as a candidate at each receiver level For a given implicit receiver of type `T`, candidates include: - **Member functions** declared on `T` (and its supertypes). - **Extension functions** on `T` that are *in scope* (defined locally or imported). So an extension on the inner receiver competes at the inner level — and therefore **beats** any member of an outer receiver, because the inner level is exhausted first. ## Member vs extension within one receiver If, at the *same* receiver, both a member and an applicable extension match, the **member wins**. Extensions never override members; they only add reach where no applicable member exists. (Signature applicability still matters — a member that doesn't actually apply won't block a fitting extension.) ## Putting the order together Roughly, for an unqualified call with receivers stacked Inner (closest) → Outer: 1. Inner receiver — its members, then extensions on Inner. 2. Outer receiver — its members, then extensions on Outer. 3. (Top-level / imported plain functions with no receiver, etc.) Proximity dominates; member-over-extension breaks ties **within** a level. ```kotlin class Inner class Outer { fun hit() = "outer-member" } fun Inner.hit() = "inner-extension" // extension on the INNER receiver fun outer(b: Outer.() -> Unit) = Outer().b() fun Outer.inner(b: Inner.() -> Unit) = Inner().b() outer { inner { hit() // "inner-extension": // Inner level is searched first; the extension on Inner // applies, so Outer.hit() (a member, but farther) loses. } } ``` The inner **extension** outranks the outer **member** purely because the inner receiver is nearer — proximity is checked before the member/extension distinction. ## Overriding the default order - **`[email protected]()`** forces the outer receiver, bypassing proximity. - **`@DslMarker`** on the receiver types makes the compiler *reject* an unqualified call that would silently bind to an outer same-marker receiver, pushing you to qualify — a deliberate guard against accidental cross-level calls. ## Why this matters in real DSLs Deeply nested builders (HTML, Gradle, kotlinx.html) routinely have name collisions across levels. Knowing that (a) inner wins, (b) inner extensions beat outer members, and (c) `this@Label` plus `@DslMarker` are the controls, lets you design and debug DSLs where calls land on the intended object. ## Caveats - Exact tie-breaking in pathological multi-candidate cases is governed by the spec's overload-resolution rules; the practical, reliable mental model is **proximity first, member-over-extension within a level**. - Local functions and top-level functions also participate, but in builder code the receiver stack is what trips people up.
- At a single receiver, can an extension function override a member with the same signature?No. When both apply to the same receiver, the member takes priority; the extension is effectively unreachable for that call unless you change the receiver or qualify differently.
- Why can an extension on the inner receiver beat a member declared on the outer receiver?Because resolution is proximity-first: the inner receiver's whole candidate set (members + applicable extensions) is considered before the outer receiver is consulted at all.
- How do you make these cross-level mistakes compile errors rather than silent bugs?Annotate the DSL's receiver types with a @DslMarker annotation; the compiler then rejects unqualified calls that would bind to an outer same-marker receiver, forcing this@Label.
saying these in an interview costs you the question
- Claiming members always beat extensions regardless of receiver proximity
- Saying an outer member always wins over an inner extension
- Ignoring that extensions must be in scope (imported/local) to be candidates
- Asserting extensions can override same-signature members
- Not mentioning this@Label / @DslMarker as the controls