skip to content

Inside a class member that is also an extension (a function with both a dispatch receiver and an extension receiver), which receiver is dispatched virtually and which is resolved statically? What surprises arise?

level: seniorimportance: should knowfreq 30%

answer

  1. Dispatch receiver = virtual
  2. Extension receiver = static
  3. Member-extension mixes both
  4. Override the enclosing class, not the extended type
  5. No free double dispatch

basics

~20 s

The class instance (dispatch receiver) is polymorphic and chosen at runtime. The extension receiver (the type after the dot) is fixed at compile time. So overriding can change the object's behavior but not which extension target is picked.

solid answer

~50 s

An extension function declared **inside a class** has two receivers: the **dispatch receiver** (the enclosing class instance) and the **extension receiver** (the type it extends). The dispatch receiver behaves like a normal member — if the function is `open` and `override`n, it is dispatched **virtually** on the runtime class of the enclosing instance. The extension receiver is resolved **statically** from its declared type, exactly like a top-level extension. So you can override the function in a subclass of the enclosing class and change behavior, but you **cannot** make it pick a different extension-receiver overload by subtyping the extension receiver. This split is a classic interview gotcha: people expect both to be virtual or both static. Concretely, `D().foo(BaseExtArg)` versus a subtype of the extension receiver won't change the chosen function; only changing the enclosing instance's runtime type triggers an override.

code

kotlin · 16 lines
kotlin
open class Base
class Derived : Base()

open class Dispatcher {
    open fun Base.extend() = "Dispatcher + Base"
    open fun Derived.extend() = "Dispatcher + Derived"
    fun call(b: Base) = b.extend() // ext receiver static -> Base.extend
}
class SubDispatcher : Dispatcher() {
    override fun Base.extend() = "SubDispatcher + Base"
}

fun main() {
    val d: Dispatcher = SubDispatcher()
    println(d.call(Derived())) // "SubDispatcher + Base"
}

go deeper

for a junior

Likely unfamiliar; can at most repeat that extensions are static — acceptable to flag this as advanced.

for a middle

Knows extensions are static but may wrongly extend that to the enclosing-class receiver too.

for a senior

Correctly separates the two receivers and predicts 'SubDispatcher + Base' with reasoning.

for a principal

Uses this precisely when designing scoped DSLs and explains the absence of double dispatch as an intentional, predictable rule.

## Two receivers, two rules Kotlin lets you declare an extension **inside a class**. Such a function has: - **Dispatch receiver** — the enclosing class instance (`this` as the class). Like any member, if `open`/`override`n it is **virtually dispatched** on the runtime type of that instance. - **Extension receiver** — the type after the dot at the call site. Like any extension, it is **statically resolved** from its declared type. This is the only place in Kotlin where one call mixes both dispatch styles. ```kotlin open class Base { open fun caller(): String = "base" } class Derived : Base() { override fun caller(): String = "derived" } open class Dispatcher { open fun Base.extend(): String = "Dispatcher + Base" open fun Derived.extend(): String = "Dispatcher + Derived" fun call(b: Base): String = b.extend() // extension receiver is statically Base } class SubDispatcher : Dispatcher() { override fun Base.extend(): String = "SubDispatcher + Base" } fun main() { val d: Dispatcher = SubDispatcher() println(d.call(Derived())) // -> "SubDispatcher + Base" } ``` ## Why "SubDispatcher + Base" - **Extension receiver static:** inside `call`, the argument's declared type is `Base`, so `Base.extend` is selected at compile time — the runtime `Derived` is ignored. That's why it is `+ Base`, not `+ Derived`. - **Dispatch receiver virtual:** the enclosing instance `d` is really a `SubDispatcher`, so the **override** of `Base.extend` runs — that's why it is `SubDispatcher`, not `Dispatcher`. ## The mental model | Receiver | Resolution | Driven by | |---|---|---| | Dispatch (enclosing class) | Virtual (runtime) | actual class of the instance | | Extension (after the dot) | Static (compile-time) | declared type of the argument | ## Surprises this causes - Overriding a member-extension changes behavior **only** through the enclosing-class hierarchy, not through the extended type's hierarchy. - You can't get "double dispatch" for free; the extension half is always static. - The `@DslMarker`/scoped-extension patterns rely on the dispatch receiver being implicit; understanding which `this` is virtual matters when designing DSLs. ## Takeaway Dispatch receiver = virtual, extension receiver = static. If a subtype of the **extended** type should change behavior, you need an explicit overload chosen at a call site where the declared type is that subtype — subtyping alone won't do it.

  • How would you make the call print "... + Derived"?
    Pass/declare the argument with static type `Derived` at the call site (e.g. `call` parameter typed `Derived`, or a generic call where the inferred type is `Derived`), so the compiler selects the `Derived.extend` overload.
  • Is this the only place Kotlin mixes virtual and static dispatch in one call?
    Yes — a member declared as an extension is the unique construct with both a virtual dispatch receiver and a static extension receiver.

saying these in an interview costs you the question

  • Saying both receivers are virtual (predicting '...+ Derived')
  • Saying both receivers are static (predicting 'Dispatcher + Base')
  • Claiming you get double dispatch automatically
  • Confusing dispatch receiver with extension receiver
  • Not knowing member-extensions can be overridden at all

context