Extensions in Kotlin
This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.
The most intuitive way to define methods and properties is inside classes. Such elements are called class members or, more concretely, member functions and member properties.
On the other hand, Kotlin allows another way to define functions and properties that are called on an instance: extensions. Extension functions are defined like regular functions, but they additionally have an extra type (and dot) before the function name. In the example below, the call
function is defined as an extension function on Telephone
, so it needs to be called on an instance of this type.
Both member functions and extension functions are referred to as methods.
Extension functions can be defined on types we don’t control, for instance String
. This gives us the power to extend external APIs with our own functions.
Take a look at the example above. We defined the extension function remove
on String
, so we need to call this function on an object of type String
. Inside the function, we reference this object using the this
keyword, just like inside member functions. The this
keyword can also be used implicitly.
The this
keyword is known as the receiver. Inside extension functions, we call it an extension receiver. Inside member functions, we call it a dispatch receiver. The type we extend with the extension function is called the receiver type.
Extension functions behave a lot like member functions. When developers learn this, they are often concerned about objects' safety, but this isn’t a problem as extensions do not have any special access to class elements. The only difference between top-level extension functions and other top-level functions is that they are called on an instance instead of receiving this instance as a regular argument. To see this more clearly, let's take a look under the hood of extension functions.
Extension functions under the hood
To understand extension functions, let's again use "Tools > Kotlin > Show Kotlin bytecode" and "Decompile" (as explained in chapter Your first program in Kotlin in section What is under the hood on JVM?). We will compile and decompile to Java our remove
function definition and its call:
As a result, you should see the following code:
public final class PlaygroundKt {
@NotNull
public static final String remove(
@NotNull String $this$remove,
@NotNull String value
) {
// parameters not-null checks
return StringsKt.replace$default(
$this$remove,
value,
""
// plus default values
);
}
public static final void main(@NotNull String[] args) {
// parameter not-null check
String var1 = remove("A B C", " ");
System.out.println(var1);
}
}
Notice what happened to the receiver type: it became a parameter. You can also see that, under the hood, remove
is not called on an object. It is just a regular static function.
When you define an extension function, you do not really add anything to a class. It is just syntactic sugar. Let's compare the two following implementations of remove
.
Under the hood, they are nearly identical. The difference is in how Kotlin expects them to be called. Regular functions receive all their arguments in regular argument positions. Extension functions are called "on" a value.
Extension properties
An extension cannot hold a state, so it cannot have fields. Although properties do not need fields, they can be defined by their getters and setters. This is why we can define extension properties if they do not need a backing field and are defined by accessors.
Extension properties are very popular on Android, where accessing different services is complex and repetitive. Defining extension properties lets us do this much more easily.
Extension properties can define both a getter and a setter. Here is an extension property that provides a different representation of a user birthdate:
Extensions vs members
The biggest difference between members and extensions in terms of use is that extensions need to be imported separately. For this reason, they can be located in a different package. This fact is used when we cannot add a member ourselves. It is also used in projects designed to separate data and behavior. Properties with fields need to be located in a class, but methods can be located separately as long as they only access the public API of the class.
Thanks to the fact that extensions need to be imported, we can have many extensions with the same name for the same type. This is good because different libraries can provide extra methods without causing a conflict. On the other hand, it would be dangerous to have two extensions with the same name but different behaviors. If you have such a situation, it is a code smell and is a clue that someone has abused the extension function capability.
Another significant difference is that extensions are not virtual, meaning that they cannot be redefined in derived classes. This is why if you have an extension defined on both a supertype and a subtype, the compiler decides which function is chosen based on how the variable is typed, not what its actual class is.
The behavior of extension functions is different from member functions. Member functions are virtual, so up-casting the type of an object does not influence which member function is chosen.
This behavior is the result of the fact that extension functions are compiled under the hood into normal functions in which the extension’s receiver is placed as the first argument:
Another consequence of what extensions are under the hood is that we define extensions on types, not on classes. This gives us more freedom. For instance, we can define an extension on a nullable or generic type:
The last important difference is that extensions are not listed as members in the class reference. This is why they are not considered by annotation processors; it is also why, when we process a class using annotation processing, we cannot extract elements that should be processed into extensions. On the other hand, if we extract non-essential elements into extensions, we don’t need to worry about them being seen by those processors. We don’t need to hide them because they are not in the class they extend anyway.
Extension functions on object declarations
We can define extensions on object declarations.
To define an extension function on a companion object, we need to use the companion object's real name. If this name is not set explicitly, the default one is "Companion". To define an extension function on a companion object, this companion object must exist. This is why some classes define companion objects without bodies.
Member extension functions
It is possible to define extension functions inside classes. Such functions are known as member extension functions.
Member extension functions are considered a code smell, and we should avoid using them if we don’t have a good reason. For a deeper explanation, see Effective Kotlin, Item 46: Avoid member extensions.
Use cases
The most important use-case for extensions is adding methods and properties to APIs that we don't control. A good example is displaying a toast or hiding a view on Android. Both these operations are unnecessarily complicated, so we like to define extensions to simplify them.
However, there are also cases where we prefer to use extensions instead of members. Consider the Iterable
interface, which has only one member function, iterator
; however, it has many methods, which are defined in the standard library as extensions1, like onEach
or joinToString
. The fact that these are defined as extensions allows for smaller and more concise interfaces.
Extension functions are also more elastic than regular functions. This is mainly because they are defined on types, so we can define extensions on types like Iterable<Int>
or Iterable<T>
.
In bigger projects, we often have similar classes for different parts of our application. Let's say that you implement a backend for an online shop, and you have a class Product
to represent all the products.
You also have a similar (but not identical) class called ProductJson
, which is used to represent the objects you use in your application API responses or that you read from API requests.
Instances of Product
are used in your application, and instances of ProductJson
are used in the API. These objects need to be separated because, for instance, you don’t want to change your API response when you change a property name in an internal class. Yet, we often need to transform between Product
and ProductJson
. For this, we could define a member function toProduct
.
Not everyone likes this solution as it makes ProductJson
bigger and more complicated. It is also not useful in transforming from Product
to ProductJson
because in most modern architectures we don’t want domain classes (like Product
) to be aware of details such as their API representation. A better solution is to define both toProduct
and toProductJson
as extension functions, then locate them together next to the ProductJson
class. It is good to locate those transformation functions next to each other, because they have a lot in common.
This seems to be a popular pattern, both on the backend and in Android applications.
Summary
In this chapter, we've learned about extensions - a powerful Kotlin feature that is often used to create convenient and meaningful utils and to control our code better. However, with great power comes great responsibility. We should not be worried about using extensions, but we should use them consciously and only where they make sense.
In the next chapter, we will finally introduce collections so that we can talk about lists, sets, maps, and arrays. There’s a lot ahead, so get ready.
Roman Elizarov (Project Lead for the Kotlin Programming Language) refers to this as an extension-oriented design in the standard library. Source: elizarov.medium.com/extension-oriented-design-13f4f27deaee