kotlinpropertiesgetterdelegation

Kotlin property delegation not working as expected


I'm confused about the different behaviour depending whether I use getters or delegated properties. Consider the following:

class Test {
    class Parts(val a: String, val b: String)

    var raw = ""
    private var cachedParts: Parts? = null

    val parts: Parts
        get() {
            println("@2")
            return cachedParts
                ?: raw.split("/")
                    .let { Parts(it.getOrElse(0) { "" }, it.getOrElse(1) { "" }) }
                    .also { cachedParts = it }
        }

    // WITH GETTERS:
    val partA get() = parts.a
    val partB get() = parts.b
}

fun main() {
    val t = Test()
    println("@1")
    
    t.raw = "one/two"

    println("a=${t.partA}, b=${t.partB}")
}

This code splits the string raw into two parts the first time parts is accessed. All later calls to parts will return the cached parts, even if raw changes. Output:

@1
@2
@2
a=one, b=two

The value of raw is empty when Test is created, but the accessors aren't called until we've set raw to some string. When partA and partB are finally accessed, they contain the correct value.

If I use property delegation instead, the code no longer works:

class Test {
    class Parts(val a: String, val b: String)

    var raw = ""
    private var cachedParts: Parts? = null

    val parts: Parts
        get() {
            println("@2")
            return cachedParts
                ?: raw.split("/")
                    .let { Parts(it.getOrElse(0) { "" }, it.getOrElse(1) { "" }) }
                    .also { cachedParts = it }
        }

    // WITH DELEGATION:
    val partA by parts::a
    val partB by parts::b
}

fun main() {
    val t = Test()
    println("@1")
    
    t.raw = "one/two"

    println("a=${t.partA}, b=${t.partB}")
}

All I've changed here is that partA is now delegated to parts::a, and the same for partB. For some strange reason, partA and partB are now accessed before the value of raw is set, so cachedParts is initilized with two empty parts. Output:

@2
@2
@1
a=, b=

Can someone explain what is going on here?


Solution

  • See what your delegated properties translate to in the documentation here. For example, partA translates to:

    private val partADelegate = parts::a
    val partA: String
        get() = partADelegate.getValue(this, this::partA)
    

    Notice that the callable reference expression part::a is used to initialise partADelegate. This expression is evaluated when the instance of Test is created, before println("@1").

    To evaluate parts::a, parts must be first evaluated. After all, this is a reference to the a property of parts, not a reference to parts.

    Therefore, parts ends up being evaluated before raw gets its value.