Kotlin path · Functions and types · Topic 5 of 13

Extension functions

Not started

An extension function lets you call title.slugify() on a String you don't own. It reads like a method, and the IDE completes it like one, but the compiler turns it into a plain static function with the receiver as its first argument. There is no change to the String class and no dynamic dispatch, and that second part is the one to remember.

Why it matters: Extensions are everywhere in Kotlin and its libraries. They look like methods but are static functions, which explains both their power and their one big trap.

  1. 1A static function in disguise

    fun String.slug() compiles to a static method that takes the String as its first parameter.

  2. 2Resolved statically

    Which extension runs depends on the declared type at the call site, not the runtime type of the object.

  3. 3Members win

    If a class has a member with the same signature, the member is always called and the extension is ignored.

1

How it works

The explanation, with examples you can run.

What gets compiled

fun String.slugify(): String =    lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-') val String.wordCount: Int                     // extension property: a getter, no field    get() = split(Regex("\\s+")).count { it.isNotEmpty() } fun main() {    println("Hello, Kotlin World!".slugify())  // hello-kotlin-world    println("one two  three".wordCount)        // 3}

On the JVM, slugify becomes public static String slugify(String $this) in the file's ...Kt class. Inside the body, this is the receiver and can be left out. Extensions can be declared on nullable types too (fun String?.orDash() = this ?: "-"), which is how isNullOrEmpty() works on a null reference.

Extension properties are only computed getters (and setters): there is nowhere to store a field, because the class wasn't changed.

Static dispatch

open class Shapeclass Circle : Shape() fun Shape.name() = "shape"fun Circle.name() = "circle" fun describe(s: Shape) = s.name()           // decided here, at compile time fun main() {    println(describe(Circle()))              // shape    println(Circle().name())                 // circle}

describe is compiled against Shape, so it calls Shape.name() whatever object is passed in. A member function declared open and overridden in Circle would print circle. If you want polymorphism, use members (or when over a sealed type); if you want to add helpers to a type you don't own, use extensions.

Two related rules:

  • Members win. An extension with the same name and parameters as a member is never called.
  • Extensions are imported. An extension declared in another package has to be imported by name, which keeps them from leaking everywhere. Two libraries with the same extension name are resolved by which one you import.

Where extensions shine

Kotlin's standard library is mostly extensions: map, filter and joinToString are extensions on Iterable, so they work on any Java collection too. Android KTX and many server libraries do the same for platform APIs. A good extension is small, has no surprising side effects, and is about the receiver: Instant.toLocalDate(zone) yes, String.sendEmail() probably not.

Extensions with a receiver are also the building block for lambdas with receivers (buildString { append("x") }), which the next topics use. Kotlin 2.4 made context parameters stable: context(logger: Logger) fun save() asks for a value that must be available in the caller's context, a more explicit replacement for the old experimental context receivers.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.fun Any.kind() = "any" and fun String.kind() = "string". What does val x: Any = "hi"; println(x.kind()) print?
  2. 2.Can an extension function read a private property of the class it extends?
  3. 3.A class has fun print() and you declare fun Doc.print() as an extension with the same signature. What does doc.print() call?

0 of 3 right so far. Get all three to finish this topic.