Extension Functions
Extension functions let you add a method to a class without subclassing it — Kotlin’s answer to wanting String.trimmed() on the JDK String. They’re statically dispatched, namespaced by import, and the engine behind the entire stdlib’s expressiveness.
Receivers, scopes, infix, scope funcs
EXAMPLE
// 1) Basic extension — add a method to String
fun String.repeated(times: Int): String = this.repeat(times)
fun main() {
println("ha".repeated(3)) // hahaha
}
// Inside the body, 'this' is the receiver (the String we're called on).
// The 'this.' is optional — 'repeat(times)' would work too.
// 2) Static dispatch (NOT polymorphic)
open class Vehicle
class Car : Vehicle()
fun Vehicle.describe() = "a vehicle"
fun Car.describe() = "a car"
fun show(v: Vehicle) = println(v.describe())
fun main2() {
show(Car()) // 'a vehicle' — extension dispatched by COMPILE-TIME type
}
// Unlike open methods, extensions don't go through vtables. Keep this in mind for hierarchies.
// 3) Extension property
val String.firstLetterOrNull: Char? get() = if (isNotEmpty()) this[0] else null
"".firstLetterOrNull // null
"swift".firstLetterOrNull // 's'
// 4) Nullable receivers
fun String?.orPlaceholder(p: String = "-"): String =
if (this.isNullOrBlank()) p else this
val n: String? = null
n.orPlaceholder() // '-'
"Mara".orPlaceholder() // 'Mara'
// You can call the function on a nullable variable directly; no '?.' needed inside.
// 5) Generic extensions
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun <T : Comparable<T>> List<T>.middle(): T = this[size / 2]
listOf(1, 2, 3).secondOrNull() // 2
listOf("a", "b", "c", "d").middle() // "c"
// 6) Infix extensions — readable DSL-style calls
infix fun Int.power(exp: Int): Long {
var r = 1L; repeat(exp) { r *= this }; return r
}
2 power 10 // 1024
// Rules: one parameter, no varargs, no default values.
// 7) Extending companion objects — add static-style factories
class Money private constructor(val cents: Long, val currency: String) {
companion object
}
fun Money.Companion.usd(dollars: Double): Money = Money(/* … */ 0L, "USD")
val m = Money.usd(9.99)
// 8) Member extensions — defined inside a class, scoped to that class's instances
class HtmlRenderer {
fun String.bold(): String = "<b>$this</b>" // dispatch receiver = HtmlRenderer
fun render(): String {
return "hello".bold() // works here
}
}
// 'hello'.bold() // compile error: only available inside HtmlRenderer
// 9) Scope functions — extension functions on Any, designed for fluent code
// .let { it -> ... } — execute block, return its result; great for null safety
// .also { it -> ... } — execute side effect, return the receiver
// .apply { ... } — execute block on the receiver (this), return it
// .run { ... } — execute block on this, return the block result
// .with(x) { ... } — not an extension; receiver is bound to x for the block
val user = User(name = "mara").apply {
email = "mara@example.com"
role = "admin"
}
val idLen = user.email?.let { it.length } ?: 0
repository.save(user).also { log.info("saved user ${it.id}") }
val summary = user.run { "$name <$email>" }
// 10) Function-typed receivers — Kotlin DSLs
fun buildString(builder: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.builder()
return sb.toString()
}
val msg = buildString {
appendLine("hi")
appendLine("there")
}
// Inside the lambda 'this' is StringBuilder — no qualifier needed.
// 11) Importing extensions like normal functions
// import com.example.text.repeated
// import com.example.scope.apply // already in kotlin.stdlib
// Avoid declaring extensions on Any / String / Int in shared libraries unless they're VERY generic —
// they pollute autocomplete for everyone who imports them.
// 12) Common patterns
// Domain conversion — DTO extension
fun UserDto.toDomain(): User = User(id = id, name = name, role = Role.valueOf(role))
fun User.toDto(): UserDto = UserDto(id = id, name = name, role = role.name)
// Result/Either wrapping
fun <T> T.asSuccess(): Result<T> = Result.success(this)
fun Throwable.asFailure(): Result<Nothing> = Result.failure(this)
// Logging side-effect chain
val saved = repo.save(order).also { log.info("order ${it.id} saved") }
// 13) Limits + gotchas
// • Static dispatch — extensions don't override member functions; ambiguity favours members
// • Can't access private members of the receiver
// • Visibility tracks the file/module — internal extensions, private file-local extensions all work
// • Inline extensions on collections — read about 'inline' to avoid object allocation on lambdas
// 14) Common bugs
// • Extension shadowed by a member with the same name → silent dispatch to the member
// • Imports forgotten → 'unresolved reference' though the function exists in another package
// • Storing extension as java.lang.Function type → not callable as obj.method() from Java callers
// • Excessive extensions on String/Any → autocomplete noise; namespace by package
// • Receiver-type confusion in nested DSL builders → use Kotlin's @DslMarker to scope receivers
Why it matters
Extension functions are how Kotlin code stays expressive: they let you bolt domain methods onto JDK types without inheritance, and they power the scope functions (let, also, apply, run) that make idiomatic Kotlin so concise. Just remember they’re statically dispatched — calling one on a supertype variable picks the supertype version, not the subtype.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…