Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Kotlin Class Delegation and Property Delegation Mechanisms

Notes 1

Kotlin supports class-level delegation natively through the by keyword, eliminating boilerplate associated with manual forwarding. When a class implements an interface via delegation, all public members of that interface are automatically routed to the specified delegate object.

interface Renderer {
    fun display()
}

class ConsoleRenderer(private val data: Int) : Renderer {
    override fun display() {
        println(data)
    }
}

class ProxyRenderer(backend: Renderer) : Renderer by backend

fun main() {
    val impl = ConsoleRenderer(42)
    ProxyRenderer(impl).display()
}

If the delegating class overrides a interface member, the local implemantation takes precedence over the delegate's version. The compiler resolves calls against the override in the proxy class.

interface Messenger {
    fun sendText()
    fun sendLine()
}

class DefaultMessenger(private val payload: Int) : Messenger {
    override fun sendText() {
        print(payload)
    }
    override fun sendLine() {
        println(payload)
    }
}

class CustomMessenger(delegate: Messenger) : Messenger by delegate {
    override fun sendText() {
        print("proxy")
    }
}

fun main() {
    val base = DefaultMessenger(10)
    CustomMessenger(base).sendText()  // prints "proxy"
    CustomMessenger(base).sendLine()  // prints "10"
}

Overrides in the delegating class remain isolated from the delegate object. The delegate cannot access the proxy's overridden properties; it operates solely on its own members.

interface Logger {
    val label: String
    fun log()
}

class FileLogger(private val id: Int) : Logger {
    override val label = "FileLogger: id=$id"
    override fun log() {
        println(label)
    }
}

class WrappedLogger(source: Logger) : Logger by source {
    override val label = "WrappedLogger override"
}

fun main() {
    val core = FileLogger(5)
    val wrapper = WrappedLogger(core)
    wrapper.log()          // prints "FileLogger: id=5"
    println(wrapper.label) // prints "WrappedLogger override"
}

On the JVM, delegating an interface that declares default methods may invoke the default implementation even when the runtime delegate type provides its own override. This behavior specifically concerns Java interop with Kotlin interfaces marked @JvmDefault.

Delegated properties allow moving common property patterns into reusable components. Typical scenarios include lazy initialization, change observation, and backing fields stored inside maps. Kotlin delegates property accessors to an object following the val/var <name>: <Type> by <expression> syntax.

class Container {
    var content: String by ContentManager()
}

The delegate must supply an operator fun getValue(). For mutable properties, it also needs operator fun setValue().

import kotlin.reflect.KProperty

class ContentManager {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef delegated '${property.name}'"
    }

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

Reading content triggers getValue, supplying the enclosing instance and a reflection descriptor of the property. Writing triggers setValue with the new value as the third argument.

Since Kotlin 1.1, local variables can also be delegated.

fun evaluate(factory: () -> Data) {
    val cached by lazy(factory)
    if (condition && cached.isReady()) {
        cached.process()
    }
}

Standard library factories cover frequent use cases. lazy accepts a lambda and returns a delegate that executes the lambda exactly once.

val cachedGreeting: String by lazy {
    println("Computing greeting")
    "Welcome"
}

fun main() {
    println(cachedGreeting)
    println(cachedGreeting)
}

By default, lazy evaluation uses synchronization so that the initializer runs in at most one thread. For lock-free initialization where multiple threads may compute simultaneously, pass LazyThreadSafetyMode.PUBLICATION. When thread safety is unnecessary and the property is always accessed from a single thread, use LazyThreadSafetyMode.NONE to avoid synchronization overhead.

Observable properties notify a handler after each assignment.

import kotlin.properties.Delegates

class Profile {
    var status: String by Delegates.observable("offline") { _, previous, current ->
        println("Status changed: $previous -> $current")
    }
}

To entercept assignments before they take effect, use vetoable instead of observable.

Properties can also be backed by a map, which is useful when reading dynamic data such as parsed JSON.

class Person(data: Map<String, Any?>) {
    val firstName: String by data
    val years: Int by data
}

fun main() {
    val person = Person(mapOf("firstName" to "Jane", "years" to 30))
    println(person.firstName)
    println(person.years)
}

Mutable properties require a MutableMap.

class MutablePerson(data: MutableMap<String, Any?>) {
    var firstName: String by data
    var years: Int by data
}

The delegate contract requires specific signatures. For read-only properties, the delegate provides:

class Session {
    val authToken: Token by TokenProvider()
}

class TokenProvider {
    operator fun getValue(session: Session, prop: KProperty<*>): Token {
        return Token()
    }
}

For mutable properties, setValue is required:

class Session {
    var authToken: Token by TokenStore()
}

class TokenStore(private var token: Token = Token()) {
    operator fun getValue(session: Session, prop: KProperty<*>): Token = token
    operator fun setValue(session: Session, prop: KProperty<*>, value: Any?) {
        if (value is Token) token = value
    }
}

Both functions may be members or extension functions and must be marked with operator. Alternatively, delegates may implement ReadOnlyProperty<in R, out T> or ReadWriteProperty<in R, T> from the standard library.

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

Under the hood, the compiler synthesizes a hidden backing property. For a property named field delegated to FieldHandler, the generated bytecode resembles:

class Container {
    private val field$delegate = FieldHandler()
    var field: String
        get() = field$delegate.getValue(this, this::field)
        set(value) = field$delegate.setValue(this, this::field, value)
}

The this::field reference is a KProperty<*> instance describing the property itself. Bound callable references have been supported since Kotlin 1.1.

Kotlin 1.1 introduced provideDelegate, allowing logic to run when the delegate is bound to a property, before any getter or setter is invoked. If the expression after by defines provideDelegate, the compiler calls it to obtain the actual delegate instance.

class ConfigValue<T> : ReadOnlyProperty<AppSettings, T> {
    override fun getValue(thisRef: AppSettings, property: KProperty<*>): T {
        TODO()
    }
}

class ConfigLoader<T>(private val key: String) {
    operator fun provideDelegate(
        thisRef: AppSettings,
        property: KProperty<*>
    ): ReadOnlyProperty<AppSettings, T> {
        checkKey(property.name)
        return ConfigValue()
    }

    private fun checkKey(name: String) { /* validation */ }
}

class AppSettings {
    fun <T> load(key: String): ConfigLoader<T> = ConfigLoader(key)
    val host by load<String>("server.host")
    val port by load<Int>("server.port")
}

Without provideDelegate, property names would need to be passed explicitly to perform validation during initialization. The generated code now initializes the synthetic delegate via provideDelegate:

class Container {
    var field: String by FieldHandler()
}

// Compiler-generated equivalent:
class Container {
    private val field$delegate = FieldHandler().provideDelegate(this, this::field)
    var field: String
        get() = field$delegate.getValue(this, this::field)
        set(value) = field$delegate.setValue(this, this::field, value)
}

Note that provideDelegate affects only delegate creation; the getter and setter generation remains unchanged.

Tags: kotlin

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.