[Doc Review]Delegated properties

With some common kinds of properties, even though you can implement them manually every time you need them, it is more helpful to implement them once, add them to a library, and reuse them later. For example:

  • Lazy properties: the value is computed only on first access.

  • Observable properties: listeners are notified about changes to this property.

  • Storing properties in a map instead of a separate field for each property.

To cover these (and other) cases, Kotlin supports delegated properties:

class Example {
    var p: String by Delegate()
}
  • The syntax is: val/var : by .

The expression after by is a delegate, because the get() (and set()) that correspond to the property will be delegated to its getValue() and setValue() methods.

For example:

import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

Standard delegates

The Kotlin standard library provides factory methods for several useful kinds of delegates.

Lazy properties

lazy() is a function that takes a lambda and returns an instance of Lazy, which can serve as a delegate for implementing a lazy property.

The first call to get() executes the lambda passed to lazy() and remembers the result. Subsequent calls to get() simply return the remembered result.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}
fun main() {
    println(lazyValue)
    println(lazyValue)
}

The result:

computed!
Hello
Hello

The print() function is invoked only during the initial call, while lazyValue retains the value "Hello" for subsequent calls.

\> Lazy is a interface

Ref.: github.com/JetBrains/kotlin/blob/0938b46726..

lazy is a function that initialize a new instance of the Lazy

fun <T> lazy(initializer: () -> T): Lazy<T>

By default, the evaluation of lazy properties is synchronized: the value is computed only in one thread, but all threads will see the same value.

If the synchronization of the initialization delegate is not required to allow multiple threads to execute it simultaneously, pass LazyThreadSafetyMode.PUBLICATION as a parameter to lazy().

Observable properties

Delegates.observable() takes two arguments: the initial value and a handler for modifications in a lambda function.

Delegates.observable() is a inline function of Delegates object. It returns a property delegate for a read/write property that calls a specified callback function when changed.

Ref.: github.com/JetBrains/kotlin/blob/0938b46726..

  • The lambda function is called when initial value changed

The handler is called every time you assign to the property (after the assignment has been performed).

It has three parameters: the property being assigned to, the old value, and the new value:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}

The result is

<no name> -> first
first -> second

If you want to intercept assignments and veto them, use vetoable() instead of observable(). The handler passed to vetoable will be called before the assignment of a new property value.

  • You can specify a condition to determine whether a value should be assigned to the Delegate property or not by using vetoable()

Delegating to another property

A property can delegate its getter and setter to another property. Such delegation is available for both top-level and class properties (member and extension). The delegate property can be:

  • A top-level property

  • A member or an extension property of the same class

  • A member or an extension property of another class

To delegate a property to another property, use the :: qualifier in the delegate name, for example, this::delegate or MyClass::delegate.

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

This may be useful, for example, when you want to rename a property in a backward-compatible way: introduce a new property, annotate the old one with the @Deprecated annotation, and delegate its implementation.

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // Notification: 'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

The result is

42

This approach allows you to continue using the deprecated property.

Storing properties in a map

One common use case is storing the values of properties in a map. This comes up often in applications for things like parsing JSON or performing other dynamic tasks. In this case, you can use the map instance itself as the delegate for a delegated property.

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

In this example, the constructor takes a map:

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

Delegated properties take values from this map through string keys, which are associated with the names of properties:

In other words, the properties in User are initialized with the map instance using keys as a direct mapping to the User property names.

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

This also works for var's properties if you use a MutableMap instead of a read-only Map:

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

Local delegated properties

You can declare local variables as delegated properties. For example, you can make a local variable lazy:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

The memoizedFoo variable will be computed on first access only. If someCondition fails, the variable won't be computed at all.

Similar to the previous case, there are situations where you don't need to initialize a property declared with by.

Property delegate requirements

For a read-only property (val), a delegate should provide an operator function getValue() with the following parameters:

  • thisRef must be the same type as, or a supertype of, the property owner (for extension properties, it should be the type being extended).

  • property must be of type KProperty<*> or its supertype.

import kotlin.reflect.KProperty

class Resource

class Owner {
    val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        println(property.name) // Prints: valResource (the name of the delegated property)
        println(property.returnType) // Prints: Resource(the type of the return value)
        return Resource()
    }
}

fun main() {
    val owner = Owner()
    println(owner.valResource) // Prints: Resource@... (the toString() representation of the created Resource instance)
}

For a mutable property (var), a delegate has to additionally provide an operator function setValue() with the following parameters:

  • thisRef must be the same type as, or a supertype of, the property owner (for extension properties, it should be the type being extended).

  • property must be of type KProperty<*> or its supertype.

  • value must be of the same type as the property (or its supertype).

class Resource

class Owner {
    var varResource: Resource by ResourceDelegate()
}

class ResourceDelegate(private var resource: Resource = Resource()) {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        return resource
    }
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
        if (value is Resource) {
            resource = value
        }
    }
}

getValue() and/or setValue() functions can be provided either as member functions of the delegate class or as extension functions. The latter is handy when you need to delegate a property to an object that doesn't originally provide these functions. Both of the functions need to be marked with the operator keyword.

You can create delegates as anonymous objects without creating new classes, by using the interfaces ReadOnlyProperty and ReadWriteProperty from the Kotlin standard library. They provide the required methods: getValue() is declared in ReadOnlyProperty; ReadWriteProperty extends it and adds setValue(). This means you can pass a ReadWriteProperty whenever a ReadOnlyProperty is expected.

fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
    object : ReadWriteProperty<Any?, Resource> {
        var curValue = resource
        override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
            curValue = value
        }
    }

val readOnlyResource: Resource by resourceDelegate()  // Typecasted to ReadOnlyProperty type
var readWriteResource: Resource by resourceDelegate()

Translation rules for delegated properties

Under the hood, the Kotlin compiler generates auxiliary properties for some kinds of delegated properties and then delegates to them.

For the optimization purposes, the compiler does not generate auxiliary properties in several cases.

For example, for the property prop it generates the hidden property prop$delegate, and the code of the accessors simply delegates to this additional property:

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

The preceding code illustrates how the delegation is internally handled. The compiler generates the prop$delegate variable and initializes it with an instance of the delegate class. This initialized prop$delegate variable is then efficiently utilized in both get() and set() methods, conserving resources.

The Kotlin compiler provides all the necessary information about prop in the arguments: > the first argument `this` refers to **an instance of the outer class C**, and `this::prop` is **a reflection object of the KProperty** type describing prop itself.

Optimized cases for delegated properties

The $delegate field will be omitted if a delegate is:

  • A referenced property:
class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}
  • A named object:
object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}

val s: String by NamedObject
  • A final val property with a backing field and a default getter in the same module:
val impl: ReadOnlyProperty<Any?, String> = ...

class A {
    val s: String by impl
}
  • A constant expression, enum entry, this, null. The example of this:
class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
}

Translation rules when delegating to another property

When delegating to another property, the Kotlin compiler generates immediate access to the referenced property. This means that the compiler doesn't generate the field prop$delegate. This optimization helps save memory.

Take the following code, for example:

class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}

Property accessors of the prop variable invoke the impl variable directly, skipping the delegated property's getValue and setValue operators, and thus the KProperty reference object is not needed.

For the code above, the compiler generates the following code:

class C<Type> {
    private var impl: Type = ...

    var prop: Type
        get() = impl
        set(value) {
            impl = value
        }

    fun getProp$delegate(): Type = impl // This method is needed only for reflection
}

Providing a delegate

By defining the provideDelegate operator, you can extend the logic for creating the object to which the property implementation is delegated.

(= By defining the provideDelegate operator, you can extend the logic for creating the object which the property implementation is delegated to.)

...

References

https://kotlinlang.org/docs/delegated-properties.html