Why does Kotlin give member functions priority over same-signature extension functions? Tie your answer to how extensions are dispatched.
answer
- Extensions = static dispatch on declared type
- Members = real methods, can be virtual
- Extension = static fn with receiver as 1st arg
- Member-wins prevents import-based hijacking
- Preserves binary compatibility
basics
~20 sExtensions are resolved by the compiler from the static type, not by the object at runtime. Letting them beat members would let any import silently change a class's behavior, so the member is kept authoritative.
solid answer
~50 sExtensions are **statically dispatched**: the compiler decides which extension applies based on the *declared (static) type* of the receiver expression and the imports in scope — there is no virtual lookup at runtime, and the class is never modified. Members, by contrast, are real methods on the type and (for `open` ones) are **dynamically dispatched**. Because an extension is just syntactic sugar over a static utility function taking the receiver as a hidden first parameter, allowing it to win over a member would mean any third-party extension import could silently override genuine behavior of a class you don't own — a correctness and security hazard. Kotlin therefore fixes the rule: on an exact-signature collision the member wins. This also preserves **binary compatibility**: adding a member later won't be defeated by pre-existing extensions in callers, and a call site's meaning stays stable as long as the member exists.
code
kotlin · 10 linesopen class A
class B : A()
fun A.tag() = "A"
fun B.tag() = "B"
fun main() {
val x: A = B()
println(x.tag()) // "A": static type wins, proving extensions aren't virtual
}go deeper
Knows extensions are static and members win, even if fuzzy on dispatch terminology.
Clearly distinguishes static vs dynamic dispatch and shows the static-type example.
Articulates the encapsulation/hijacking rationale and the binary-compatibility benefit.
Discusses contract ownership, evolution of public APIs, and how this rule shapes safe library layering and deprecation strategy.
## Two different dispatch models **Member functions** are part of the type. If declared `open` and overridden, they are **dynamically dispatched** (virtual): the *runtime* type of the object decides which override runs. **Extension functions** are **statically dispatched**. The compiler picks the extension using: - the **static (declared) type** of the receiver expression, and - which extensions are **imported / in scope**. Under the hood, an extension compiles to a plain static function whose first parameter is the receiver: ```kotlin fun String.shout(): String = uppercase() // roughly compiles to: // fun shout(receiver: String): String = receiver.uppercase() ``` The class `String` is **not touched**; nothing is added to it. ## Why the member must win If an extension could beat a member, then merely **importing** an extension — possibly from a library you don't control — could **silently change** what `obj.foo()` does. That breaks encapsulation: the class author no longer owns the contract of `foo()`. By making the member authoritative on an exact-signature collision, Kotlin guarantees: - **Class authors keep control** of their own behavior. - **No accidental hijacking** by transitive imports. - **Binary/source stability:** if a library later *adds* a member matching your extension, callers transparently switch to the member — your extension just goes quiet — instead of silently doing the wrong thing. ## Static dispatch demonstrated Because extensions resolve on the *static* type, the declared type of the variable — not the object — chooses the extension: ```kotlin open class A class B : A() fun A.tag() = "A" fun B.tag() = "B" val x: A = B() println(x.tag()) // "A" — static type A decides, not runtime type B ``` Contrast with a member, which would dispatch on the runtime type. This same static nature is exactly why an extension can't be trusted to override a member — and why the language doesn't let it. ## Edge: nullable receiver still doesn't override Even an extension on a nullable receiver (`fun Greeter?.hello()`) does not override a member `hello()`; for a non-null instance the member still wins for the matching signature.
- Given extensions dispatch on the static type, what does `(x as B).tag()` print for the example above?"B" — casting changes the static type seen by the compiler, so the `B` extension is chosen. It's still a compile-time decision, just on a different declared type.
- How does the member-wins rule help binary compatibility?A library can add a member that matches a caller's pre-existing extension; the caller transparently switches to the member instead of silently running stale extension logic.
Members are wired into the machine; extensions are stickers on the outside. The compiler reads the sticker by the label on the box, not by what's really inside.
saying these in an interview costs you the question
- Saying extensions are virtual / dynamically dispatched
- Claiming the extension modifies or is added to the class
- Thinking dispatch uses the runtime type for extensions
- Not connecting the rule to import-based hijacking risk
- Believing precedence is configurable per call