thecodex.expert · The Codex Family of Knowledge
Kotlin

Extension Functions

An extension function is resolved by the variable’s DECLARED type at compile time, not its actual runtime type — a genuinely different, easy-to-miss rule compared to how real member functions dispatch.

Kotlin 2.1 Resolved statically, not dynamically Last verified:
Canonical Definition

fun String.shout() = this.uppercase() + "!" defines an extension function, letting any String call .shout() as though it were a real member — without modifying String's own source, which for a built-in or third-party class isn't even possible. Inside the extension, this refers to the receiver (the String being called on). Critically, extension functions are resolved STATICALLY, based on the variable's declared compile-time type, not its actual runtime type — a real, if uncommon, source of confusion when an extension function and a real member function share a name, since the member function always wins.

A basic extension function

shout() is called exactly like a real method, but String's own source was never touched — this is precisely how it's possible to "add" a method to a class you don't own at all, like a class from a third-party library.

Kotlinbasic_extension.kt
fun String.shout(): String {
    return this.uppercase() + "!"   // 'this' refers to the String being called on
}

println("hello".shout())   // "HELLO!"

Extension properties

Extension properties work the same way, though they can't have backing fields — they must be computed from the receiver each time, since there's no place to actually store extra state on a class you don't own.

Kotlinextension_property.kt
val String.lastChar: Char
    get() = this[this.length - 1]

println("hello".lastChar)   // 'o'

The real gotcha: static resolution, not dynamic dispatch

printType is chosen by v's DECLARED type (Base) at compile time, even though v actually holds a Derived at runtime — genuinely different from how a real overridden member function would resolve, which would use Derived's version through dynamic dispatch.

Kotlinstatic_resolution.kt
open class Base
class Derived : Base()

fun Base.printType() = println("Base")
fun Derived.printType() = println("Derived")

val v: Base = Derived()   // declared type: Base, actual type: Derived
v.printType()               // prints "Base" — resolved by the DECLARED type, not the actual one!

Sources

1
JetBrains. Kotlin Documentation, "Extension functions," kotlinlang.org/docs/extensions.html.