Lesson 17. Delegation
Mission link: Lesson 12 named composition over inheritance as the fit for shared behavior across unrelated types; class delegation is Kotlin's native syntax for the specific composition pattern of "implement this interface by forwarding to an object I hold," with zero hand-written forwarding methods.
Primary source: Docs: "Delegation", Kotlin
Prerequisites: Lesson 16, Platform type
Warm-up
- ▢ Why is a bare
returninside a lambda normally a compile error, and what makes it legal inside an inline function's lambda?
Check
Normally the lambda is its own separate function object, so a bare return has no enclosing function to exit at that point. If the function the lambda is passed to is inlined, the lambda's code is pasted directly into the calling function's body, so the return genuinely exits that enclosing function, a legal non-local return.
- ▢ Why is
reifiedonly legal on an inline function's type parameter?
Check
Ordinary generic type parameters are erased at runtime; inlining removes this erasure by pasting the function's body directly at each call site, where the concrete type is known at compile time, which is exactly what lets a reified type parameter be used almost like a normal class inside the function.
Know this
Class delegation implements an interface by forwarding, with the compiler writing the forwarding code
class Derived(b: Base) : Base by b implements the Base interface entirely by forwarding every one of its members to b, an object Derived holds internally. The by-clause tells the compiler to generate every forwarding method itself, zero boilerplate hand-written by you. This is the direct, language-native version of the classic Delegation design pattern (an alternative to inheritance where an object holds another and forwards calls to it), without needing to write out a forwarding method for every single interface member by hand.
Overriding a delegated member works exactly as expected, from the outside
class Derived(b: Base) : Base by b { override fun printMessage() { print("abc") } } overrides just printMessage(); calling it on a Derived instance runs the override, not the delegate's version, while any other Base member not overridden still forwards to b as usual. This composes cleanly: delegation handles the default case, and specific overrides handle the exceptions, without needing to override every member just to change one.
The subtlety worth knowing precisely: the delegate can't see the derived class's overrides
If Base's own print() method internally calls message (another member of the same interface), and Derived overrides message, calling derived.print() still uses the delegate's own message, not Derived's override, because the delegate object (b) only ever sees its own implementation of the interface's members when calling between them internally; it has no visibility into whatever Derived overrode. This is worth internalizing precisely rather than assuming delegation behaves like inheritance (where an overridden method actually gets picked up by other methods calling it polymorphically, virtually, from within the base type); delegation is forwarding to a separate object, not extending one.
Delegated properties reuse property behavior the same way class delegation reuses interface implementation
val/var <name>: <Type> by <expression> delegates a property's get() (and set(), for var) to the expression's own getValue()/setValue() operator functions, rather than writing custom accessor logic (lesson 7) by hand on every property that needs the same pattern. by lazy { ... }, the most common built-in delegate, computes and caches a value on first access, then returns the cached value on every subsequent access, without needing to write that caching logic yourself. A custom delegate (implementing getValue()/setValue() directly) is how a pattern like "log every read and write to this property" or "store this property's value in a shared map instead of its own field" gets written once and reused across every property that needs it, rather than duplicated by hand.
Practice
- ▢ What does
class Derived(b: Base) : Base by bactually generate, and what would the equivalent hand-written code look like withoutby?
Check
It generates a forwarding method for every member of Base, each one calling the corresponding method on b. Without by, Derived would need to manually write out every one of those forwarding methods itself, one per interface member, each simply calling through to b.
- ▢
interface Base { val message: String; fun print() }, whereBaseImpl.print()internally readsmessage.class Derived(b: Base) : Base by b { override val message = "Derived's message" }. What doesderived.print()actually print, and why might that be surprising?
Hint
Consider whose implementation of message the delegate object's own print() method actually has access to when it runs.
Check
It prints the delegate object's (b's) own message, not Derived's overridden one, since b.print() only ever sees b's own implementation of message when it runs internally; it has no way to see that Derived overrode message elsewhere. This can be surprising if delegation is assumed to behave like inheritance, where an overridden member is genuinely picked up by other methods calling it virtually from within the base type.
- ▢ What does
val name: String by lazy { computeExpensiveName() }actually do, and when doescomputeExpensiveName()run?
Check
It delegates name's get() to the lazy delegate's getValue(), which computes the value (running computeExpensiveName()) only the first time name is actually read, then caches that result and returns it directly on every subsequent read without recomputing.
- ▢ What must a custom property delegate provide to support a
varproperty, and what's required for avalproperty?
Check
A val property's delegate only needs a getValue() operator function (matching the property owner's type and returning the property's type). A var property's delegate additionally needs a setValue() operator function, accepting the new value alongside the same thisRef and property parameters, since a mutable property needs both a read and a write path delegated.
-
▢ Which claim correctly describes delegation in Kotlin?
- a) Class delegation via
bybehaves identically to inheritance: a method on the delegate calling another interface member always sees the derived class's overrides - b) Class delegation forwards every interface member to a held object with compiler-generated code, but the delegate's own internal calls only see its own implementations, not the derived class's overrides; property delegation reuses accessor logic the same way, via
getValue()/setValue() - c)
by lazy { ... }recomputes its value on every read, never caching the result - d) A read-only (
val) property delegate must implement bothgetValue()andsetValue()
- a) Class delegation via
Check
b) That's the precise mechanism and the precise subtlety this lesson covers. (a) is false, exactly the misconception this lesson warns against: delegation is forwarding to a separate object, so the delegate's internal calls can't see the derived class's overrides the way genuine inheritance would. (c) is false: lazy computes once, on first access, then returns the cached result on every later read. (d) is false: a val delegate only needs getValue(); setValue() is only required for a mutable var property.
Real-world reps
- [ ] Find a use of class delegation (
by) in a codebase you've written or have access to, or construct a small example implementing an interface by delegating to a held object. Check whether the delegate's own internal method calls could ever be affected by an override in the delegating class, and confirm your understanding of why or why not. - [ ] Find a
by lazy { ... }property. Confirm you can explain exactly when its initializer block runs, and check whether it's actually being read more than once elsewhere in the code (making the caching worthwhile). - [ ] Tomorrow: read the primary source's section on property delegate requirements in full, and sketch (on paper) a small custom delegate that logs every read and write to a property.
Going further
Not landing? Reread the primary source at the top, since this lesson compresses it and compression is where understanding leaks. Check the glossary for any term that felt slippery.
If the lesson itself is unclear rather than the material, that is a defect: open an issue.