kotlindelegatesextension-methods

Why does Kotlin allow delegation to extension properties with no backing fields?


I understand why Kotlin doesn't allow initializing extension properties such as

val SomeClass.anExtensionProperty = "can't do this"

because there is no backing field to hold the value. And I understand that you can use a get()'ter because it doesn't hold state, like this:

val SomeClass.anExtensionProperty get() = "you CAN do this"

because it's calculated on the fly and there's no state to hold.

So I was surprised to find you can do this:

val SomeClass.anExtentionsionProperty by lazy{
   "you CAN do this, but why?"
}

In my mind, when using by delegation normally (not with an extension) you are "setting" a property to reference a delegate, which means storing a reference (i.e. state) to an instantiated object. That seems to go against the restrictions imposed on extension properties.

Why is this allowed? What's going on under the hood?


Solution

  • When we use the by operator, the compiler calculates the value on the right side and stores the delegate in the current variable scope. If we do this in the function, delegate is stored as a local variable. If we are in the class, it is stored as a member. If we're in the top-level scope, it is stored in the top-level scope.

    Your case is the last one. We are in the top-level scope, so we create a delegate in the top-level scope. It doesn't create separate lazy objects per each instance. It creates a single global one and shares it across all instances. We can easily verify this:

    fun main() {
        val c1 = SomeClass()
        println(c1.anExtentionsionProperty)
        val c2 = SomeClass()
        println(c2.anExtentionsionProperty)
    }
    
    class SomeClass
    
    val SomeClass.anExtentionsionProperty by lazy{
        println("hello")
        "you CAN do this, but why?"
    }
    

    This code prints the "hello" only once, because both instances share the same lazy.

    We can also verify this in the bytecode:

    public final class Test1Kt {
      private final static Lkotlin/Lazy; anExtentionsionProperty$delegate
    }
    

    It created a static field in the Test1Kt class (file was named test1.kt) and there it stored the Lazy object. Then it reads it whenever we access the anExtentionsionProperty.

    Update

    It may be confusing that the delegate has a different scope than the property itself. But I feel this happens only if we look at this code line from incorrect angle. Similarly as the code:

    val bar = getFoo() + 1
    

    is an equivalent of:

    val foo = getFoo()
    val bar = foo + 1
    

    The same happens here. Code:

    val SomeClass.anExtentionsionProperty by getFoo()
    

    Is an equivalent of:

    val foo = getFoo()
    val SomeClass.anExtentionsionProperty by foo
    

    While we are "executing" this line of code, we immediately calculate the value on the right side and implicitly "remember" it. Then we use the remembered value. Scoping is one potentially confusing part. Another is that some people might think the above code calls getFoo whenever we access the anExtentionsionProperty. Which is also not true.