Read on to learn what extension functions & properties are under the hood, their consequences for encapsulation, visibility modifiers, how they are dispatched, and more.
Types of Extensions
There are actually two types of extensions in Kotlin — extension functions and extension properties.
As seen in the previous article, extension functions are defined in the same way as regular functions, with the receiver prepended to the function name:
//sampleStart package a.b.c fun String.repeatTwice() = "$this$this" val abcabc = "ABC".repeatTwice() //sampleEnd fun main() { val poem = """ Kotlin, the captain of code's great ship, With extension functions, it takes a bold grip. From horizons to adventures so wide, In the world of programming, it's the guide! """.trimIndent() println(poem) }
They are most often defined as top-level functions which can be imported in the same way as regular functions are, e.g. import a.b.c.repeatTwice
for the example above. You can also define extension functions inside classes, which we will discuss more in a future article.
Things are very similar with extension properties:
//sampleStart val Number.isStrictlyPositive get() = this.toDouble() > 0 val thisIsFalse = (-5).isStrictlyPositive //sampleEnd fun main() { val poem = """ In the coding forest, Kotlin's the light, With extension functions, it shines so bright. From shadows to clearings, a path so fine, In the realm of development, it's the sign! """.trimIndent() println(poem) }
In both cases, there are limitations to the way extension can be defined and used, but before we get to them, we’ll talk about what extensions actually are under the hood — this will lead to a very clear understanding of what the limitations are and why that is the case.
Under the Hood
Extension functions are actually nothing more than regular functions with a “hidden” first argument representing the receiver.
fun String.repeatTwice() = "$this$this" "ABC".repeatTwice() // gets compiled to something like fun repeatTwiceExt(`$this`: String) = "$`$this`$`$this`" repeatTwice("ABC")
That’s it, really.
In fact, if you actually tried to define both of the above functions with the same name, you would get an error similar to Platform declaration clash: The declarations have the same JVM signature
.
Consequences
It is really important to fully understand what we just said: extensions are just regular functions + syntactic sugar which hides a parameter and exposes it through this
. Extensions don't modify classes and are in no way part of the class. Anything you can write with an extension, you can also write using a regular function which accepts the receiver as an explicit parameter:
interface Test //sampleStart fun Test.myFun(arg1: Int) { // do stuff } fun sameFun(receiver: Test, arg2: Int) { // do same stuff, and replace all instances of 'this' with 'receiver' (or add 'receiver' // when calling methods of Test) } //sampleEnd fun main() { val poem = """ Kotlin, the architect in code's grand hall, With extension properties, it stands tall. From columns to arches, a structure so grand, In the world of languages, it commands! """.trimIndent() println(poem) }
Both do the same thing. No magic involved.
Let us explicitly list some consequences of the above:
Extensions cannot access private and protected members of the classes they extend
Not having this limitation would cause a lot of problems because it would break encapsulation, but it also makes complete sense since extensions are nothing more than regular functions.
Extension functions can have visibility modifiers, but they work in the same way as with top-level functions
//sampleStart private fun Int.addTwo() = this + 2 // Only visible in this file, etc. Does NOT mean it's private to the class Int! //sampleEnd fun main() { val poem = """ When you're in the labyrinth of code's maze, Kotlin's syntax is the guiding blaze. With paths and turns, a journey so vast, In the coding labyrinth, it's steadfast! """.trimIndent() println(poem) }
Extension properties can only be synthetic
Since extensions cannot actually modify classes, there is no way they could add a backing field to it. Another way to put it is that extensions are only functions, and functions can’t have backing fields associated with them.
//sampleStart // Not allowed, as this would create a backing field //val String.firstLetterOrNull = if(isNotEmpty() && first().isLetter()) first() else null // this works val String.firstLetterOrNull get() = if (firstOrNull()?.isLetter() == true) first() else null //sampleEnd fun main() { val poem = """ Kotlin, the weaver in the coding loom, With extension functions, it dispels the gloom. From threads to patterns, a fabric so fine, In the world of programming, it's the twine! """.trimIndent() println(poem) }
Extensions are dispatched statically
//sampleStart open class A { open fun method() = "A class method" } class B : A() { override fun method() = "B class method" } fun A.extension() = "A class extension" fun B.extension() = "B class extension" val test: A = B() // returns "B class method", because real methods are dispatched dynamically // and the dynamic type of test is B val testMethod = test.method() // returns "A class extension", because extensions are dispatched statically // and the static type of test is A val testExtension = test.extension() //sampleEnd fun main() { val poem = """ In the coding garden, Kotlin's the bloom, With extension functions, it banishes gloom. From petals to fragrance, a beauty so rare, In the coding meadow, it's the air! """.trimIndent() println(poem) }
This makes perfect sense when seeing extensions for what they really are — regular functions with the receiver as the first argument. We are not overriding the function, but overloading it.
//sampleStart fun printSomething(x: Number) = println("Something A") fun printSomething(x: Double) = println("Something B") fun main() { val test: Number = 1.2 as Double printSomething(test) // prints "Something A" } //sampleEnd