Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Working with Extensions for Kotlin Classes and Objects

Tech 1

Kotlin allows you to add new functionality to an existing class without inheriting the target class or relying on structural patterns like the Decorator pattern. This capability is implemented via a feature called extensions. For example, you can add new functions to third-party libray classes that you cannot modify directly, and these added functions behave exactly like native member methods of the original class, callable via standard dot syntax. In addition to extension functions, Kotlin also supports extension properties to add new attributes to existing classes.

Extension Functions

To declare an extension function, you prefix the function name with the receiver type — the type that is being extended. The example below adds a swap function for mutable integer lists:

fun MutableList<Int>.swapElements(firstIdx: Int, secondIdx: Int) {
    val temp = this[firstIdx] // `this` refers to the receiver list instance
    this[firstIdx] = this[secondIdx]
    this[secondIdx] = temp
}

// Call the new extension on any MutableList<Int>
val myList = mutableListOf(1, 2, 3)
myList.swapElements(0, 2) // `this` inside the extension holds a reference to `myList`

This extension can be generalized to work with any mutable list, regardless of element type. To use generics in the receiver type declaration, declare the generic parameter before the function name:

fun <T> MutableList<T>.swapElements(firstIdx: Int, secondIdx: Int) {
    val temp = this[firstIdx]
    this[firstIdx] = this[secondIdx]
    this[secondIdx] = temp
}

Static Resolution of Extensions

Extensions do not actually modify the class they extend. Defining an extension does not insert new members into the original class, it only enables calling the new function via dot syntax on variables of the receiver type.

A core property of extensions is that they are statically dispatched, not virtual like class member methods. This means the extension function that gets called is determined by the compile-time declared type of the receiver expression, not the runtime type of the underlying object. For example:

open class Shape
class Rectangle : Shape()

fun Shape.getTypeName() = "Shape"
fun Rectangle.getTypeName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getTypeName())
}

printClassName(Rectangle())

This example outputs Shape, because the called extension is determined solely by the declared type of parameter s, which is Shape.

If a class already has a member function with the same receiver type, name, and matching parameters as an extension, the member function always takes precedence:

class Example {
    fun printMethodType() {
        println("Class method")
    }
}

fun Example.printMethodType() {
    println("Extension function")
}

Example().printMethodType()

This outputs Class method as expected.

Extensions can safely overload existing member functions if they have different signatures:

class Example {
    fun printMethodType() {
        println("Class method")
    }
}

fun Example.printMethodType(i: Int) {
    println("Extension function with argument: $i")
}

Example().printMethodType(1)

Nullable Receivers

You can define extensions for nullable receiver types. These extensions can be called even if the receiver variable is null, and you can add a this == null check inside the extension body. This enables safe calls on potentially null objects without requiring null checks at the call site. For example:

fun Any?.safeToString(): String {
    if (this == null) return "null"
    // After null checking, `this` is automatically cast to a non-null type
    // so the below call resolves to the native Any.toString()
    return toString()
}

Extension Properties

Kotlin supports extension properties just like it supports extension functions:

val <T> List<T>.lastIndex: Int 
    get() = size - 1

Since extensions do not actually insert members into the target class, extension properties cannot have backing fields. This means initializers are not allowed for extension properties, and their behavior must be defined explicitly via getters/setters:

// Error: extension properties cannot have initializers
// val House.buildingNumber = 1

Companion Object Extensions

If a class has a companion object, you can define extension funcsions and properties for the companion object. Just like native companion members, you can call these extensions using the class name as a qualifier:

class MyClass {
    companion object {} // Default name is Companion
}

fun MyClass.Companion.printCompanion() {
    println("companion")
}

fun main() {
    MyClass.printCompanion()
}

Extension Scope

Most extensions are declared at the top level, directly inside a package:

package org.example.declarations
fun List<String>.getLongestString() { /*Implementation*/ }

To use an extension declared outside the current package, you need to import it:

package org.example.usage
import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

Member Extensions

You can declare extensions for one class inside another class. In this setup, there are two implicit receivers:

  • Dispatch receiver: The instance of the class where the extension is declared
  • Extension receiver: The instance of the clas being extended

Members of both receivers can be accessed without qualifiers:

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    fun Host.printConnectionString() {
        printHostname() // Calls Host.printHostname()
        print(":")
        printPort() // Calls Connection.printPort()
    }

    fun connect() {
        host.printConnectionString()
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    // Error: Extension is not accessible outside Connection
    // Host("kotl.in").printConnectionString()
}

If there is a name conflict between the dispatch receiver and extension receiver members, the extension receiver takes priority. To reference the dispatch receiver member, use qualified this syntax:

class Connection {
    fun Host.getConnectionString() {
        toString() // Calls Host.toString() (extension receiver)
        this@Connection.toString() // Calls Connection.toString() (dispatch receiver)
    }
}

Member extensions can be declared open and overridden in subclasses. Dispatch is virtual for the dispatch receiver type, but static for the extension receiver type:

open class Base {}
class Derived : Base() {}

open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("Base extension function in BaseCaller")
    }

    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }

    fun call(b: Base) {
        b.printFunctionInfo()
    }
}

class DerivedCaller : BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }

    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}

fun main() {
    BaseCaller().call(Base()) // Output: "Base extension function in BaseCaller"
    DerivedCaller().call(Base()) // Output: "Base extension function in DerivedCaller" — virtual dispatch on dispatch receiver
    DerivedCaller().call(Derived()) // Output: "Base extension function in DerivedCaller" — static dispatch on extension receiver
}

Visibility Rules

Extensions follow the same visibility rules as other declarations in the same scope:

  • Extensions declared at the top level of a file can access other private top-level declarations in the same file
  • If an extension is declared outside the receiver type's scope, it cannot access private members of the receiver class

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.