skip to content

How do extension functions fit the Kotlin function model, and what does the receiver let you do that a plain top-level function does not? What are the key limits?

level: middleimportance: must knowfreq 65%

answer

  1. Receiver before the dot = `this`
  2. Compiles to static fn, receiver = 1st arg
  3. Resolved statically by compile-time type
  4. Member always beats extension
  5. No access to private members

basics

~20 s

An extension function looks like a method added to an existing type. You write fun String.shout() = uppercase() + "!" and call "hi".shout(). It is really a static function with the receiver as a hidden first argument; it cannot access private members and is resolved statically.

solid answer

~40 s

Extensions let you call a function with **method syntax** on a type you don't own, e.g. `fun List<Int>.second() = this[1]`. The type before the dot is the **receiver**; inside the body `this` is that instance, so the call reads `list.second()`. They keep the function model intact: an extension is compiled to a **static** function taking the receiver as the first parameter, so there is no real modification of the class. Limits: extensions are **resolved statically** by the declared (compile-time) type, not virtually — they don't override and don't participate in polymorphism; a member function with the same signature **always wins** over an extension; and they **cannot access `private`/`protected` members** of the receiver. They're for adding focused, discoverable syntax/utilities (especially on stdlib or third-party types) without inheritance or wrapper classes.

code

kotlin · 11 lines
kotlin
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

println(listOf("a", "b", "c").secondOrNull())  // b
println(listOf(1).secondOrNull())               // null

// static resolution demo
open class A; class B : A()
fun A.tag() = "A"
fun B.tag() = "B"
val ref: A = B()
println(ref.tag())  // A  (decided by declared type A)

go deeper

for a junior

Can write and call a simple extension and knows the receiver is this.

for a middle

Explains the static-function-with-receiver compilation and that members win over extensions.

for a senior

Demonstrates static (non-virtual) resolution by declared type and the no-private-access limit with examples.

for a principal

Weighs API/binary-compat implications, discoverability vs. namespace pollution, and when an extension is the wrong tool versus a real member or delegation.

## What an extension function is An **extension function** lets you call a function on an existing type using `instance.fn()` syntax, even when you cannot edit that type. The type written before the function name is the **receiver type**: ```kotlin fun String.shout(): String = this.uppercase() + "!" "hello".shout() // "HELLO!" ``` Inside the body, `this` refers to the receiver (`"hello"` here); you can omit `this`. ## How it fits the function model It is **not** real class modification. The compiler turns the above into a static function whose first parameter is the receiver — roughly `fun shout(receiver: String): String`. So it slots straight into Kotlin's first-class/top-level function story; the dot syntax is sugar over passing the receiver. ## What the receiver buys you over a plain function - **Readability / fluency**: `"a,b".split(",").map { it.trim() }` reads as a pipeline instead of nested calls. - **Discoverability**: IDE autocomplete shows `shout()` after a `String.`, so utilities are found in context. - **Targeting types you don't own**: add helpers to `String`, `List`, third-party classes — no subclass, no wrapper. ## The key limits (interview gold) 1. **Static resolution, not virtual.** Which extension runs is decided by the **compile-time type** of the expression, not the runtime type. There is no override/polymorphism: ```kotlin open class Base class Derived : Base() fun Base.name() = "base" fun Derived.name() = "derived" val x: Base = Derived() x.name() // "base" — static type Base wins ``` 2. **Members win over extensions.** If the class already has a member with the same name and signature, the **member** is always called; the extension is shadowed. 3. **No access to non-public members.** An extension cannot read `private`/`protected` fields of the receiver — it only sees what any outside caller sees. 4. **Not actually added to the class.** Reflection on the class won't list it as a method; it lives in the file's `*Kt` wrapper. ## Extension properties The model extends to **extension properties** too, but they must be computed (no backing field): ```kotlin val String.firstChar: Char get() = this[0] ``` ## When to use Reach for extensions to add focused, stateless syntax to existing types; prefer a real member when the behavior is core to the type and needs its internal state.

  • If a class has member `fun size()` and you also declare an extension `fun MyType.size()`, which runs?
    The member always wins; the extension is shadowed and never called via `instance.size()`.
  • Why can't an extension property have a backing field?
    Because the function/property isn't actually stored in the class instance; there's nowhere to keep state, so it must be a computed get (and optional set delegating elsewhere).

An extension is a sticky note added to someone else's book — it reads like part of the book, but it can't see the locked pages and never overrides what's already printed.

saying these in an interview costs you the question

  • Saying extensions modify or reopen the class
  • Claiming extensions are resolved polymorphically / can override members
  • Thinking an extension can read private fields
  • Believing extensions appear via reflection as class methods
  • Asserting extension properties can hold a backing field

context