Kotlin Extension Functions
Overview
Kotlin provides a way to extend a class with new functionality without inheriting from the class or using design patterns such as Decorator. This is done with extension declarations.
For example, you can write a new function for a class from a third-party library that you cannot modify. These functions can be called in the usual way, as if they were methods of the original class. This mechanism is called extension functions. There are also extension properties, which let you define new properties for existing classes.
You can also add functions to existing basic classes. For example, this means you can add functions to basic types such as Int and String.
fun String.hello() = println("Hello $this")
fun main() {
"devkuma".hello()
}
Extension functions
To declare an extension function, prefix the function name with the receiver type, which is the type being extended.
fun ReceiverType.functionName(parameter): ReturnType { implementation }
The following example adds a swap function to MutableList<Int>.
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' refers to MutableList<Int>.
this[index1] = this[index2]
this[index2] = tmp
}
The this keyword inside an extension function corresponds to the receiver object, the object passed before the dot in the function call. This lets you call the declared function on MutableList<Int>.
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // Inside 'swap()', 'this' has the value of 'list'.
Extension functions for generic classes
To add an extension function to a Generic class, you can define it as follows.
fun ClassName.functionName(argumentType): ReturnType { implementation }
To make the function above usable not only with Int but also with other objects, generalize it with generics as MutableList<T>.
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' refers to MutableList<T>.
this[index1] = this[index2]
this[index2] = tmp
}
Extensions are resolved statically
Extensions do not actually modify the classes they extend. Defining an extension does not insert a new member into a class; it only lets you call a new function with dot (.) notation on variables of that type.
Extension functions are dispatched statically, which means they are not virtual with respect to the receiver type. The extension function that is called is determined by the type of the expression on which the function is called, not by the runtime type produced by evaluating that expression.
For example:
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
fun main() {
printClassName(Rectangle())
}
Output:
Shape
This example prints Shape because the extension function that is called depends only on the declared type of parameter s, which is Shape.
If an extension function with the same receiver type, name, and arguments is defined for a class that already has a member function, the member function is executed.
For example:
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }
fun main() {
Example().printFunctionType()
}
Output:
Class method
This example prints the class method.
If the extension function has the same name but different arguments, it overloads the member function by adding another function, so the extension function is executed.
class Example2 {
fun printFunctionType() { println("Class method") }
}
fun Example2.printFunctionType(i: Int) { println("Extension function #$i") }
fun main() {
Example2().printFunctionType(1)
}
Nullable receivers
Extensions can also be defined with nullable receiver types. These extensions can be called on object variables even when the value is null, and inside the function they can check this == null.
Because the check is already performed inside the extension function, toString() can be called without checking for null.
fun Any?.toString(): String {
if (this == null) return "null"
// After checking for null, 'this' is automatically converted to a non-null type,
// so the following 'toString()' is resolved as a member function of Any.
return toString()
}
Run the following example.
fun main() {
println(null.toString())
}
Output:
null
toString() is executed on null, but no error (NPE) occurs and null is printed.
Extension properties
Kotlin supports extension properties just as it supports functions.
val <T> List<T>.lastIndex: Int
get() = size - 1
Since extensions do not actually insert members into classes, there is no efficient way for an extension property to have a backing field. For this reason, initialization is not allowed for extension properties. They can be defined only by explicitly providing a getter or setter.
val House.number = 1 // Error: initialization is not allowed for extension properties.
Companion object extensions
If a class defines a companion object, you can also define extension functions and properties for that companion object. As with regular members of the companion object, they can be called by using only the class name as the qualifier.
class MyClass {
companion object { } // This code is called "Companion".
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}
Output:
companion
Scope of extensions
In most cases, extensions are defined at the top level directly under a package.
package org.example.declarations
fun List<String>.getLongestString() { /*...*/ }
To use an extension outside the package where it is declared, import the extension at the call site.
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
For details, see imports.
Declaring extensions as members
You can declare an extension for one class inside another class. Inside these extensions, there are several implicit receivers whose members can be accessed without qualification. The instance of the class where the extension is declared is called the dispatch receiver, and the instance of the receiver type of the extension method is called the extension receiver.
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() // Calls the extension function.
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
//Host("kotl.in").printConnectionString() // Error: the extension function cannot be used outside 'Connection'.
}
Output:
kotl.in:443
If there is a name conflict between the dispatch receiver and the extension receiver, the extension receiver takes precedence. To refer to a member of the dispatch receiver, use qualified this syntax.
class Connection {
fun Host.getConnectionString() {
toString() // Calls 'Host.toString()'.
this@Connection.toString() // Calls 'Connection.toString()'.
}
}
Extensions declared as members can be declared open and overridden in subclasses. This means dispatch for these functions is virtual with respect to the dispatch receiver type, but static with respect to 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() // call the extension function
}
}
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()) // "Base extension function in BaseCaller"
DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}
Output:
Base extension function in BaseCaller
Base extension function in DerivedCaller
Base extension function in DerivedCaller
Note on visibility
Extensions use the same visibility modifiers as regular functions declared in the same scope. For example:
- A
privateextension declared at the top level of a file can access other top-level declarations in the same file. - If an extension is declared outside the receiver type, it cannot access the receiver’s
privateorprotectedmembers.