A junior writes a reusable `var`-delegate that stores its value in a single field on the delegate instance, then uses one delegate object across several properties. What breaks, and how do you design a correct reusable custom delegate?
answer
- Shared single-field delegate → properties alias same slot
- Idiomatic `by Box()` makes a fresh instance per property
- Shared delegate must key storage by property.name (and thisRef per owner)
- Plain field isn't thread-safe → @Volatile / AtomicReference
- Don't cache thisRef; read it each call
basics
~20 sIf one delegate object holds a single value field and is shared by several properties, they all read and write the same field, so the properties overwrite each other. Either give each property its own delegate, or key the storage by the property.
solid answer
~40 sA delegate instance holds whatever state you put in its fields. If you store the value in a single field and reuse the *same* instance for multiple properties (e.g. via a shared object or a non-fresh factory), every delegated property aliases that one field — writes to one clobber the others. Fixes: (1) create a fresh delegate per property (`var a by Box(); var b by Box()` — two instances, the usual case), or (2) make one shared delegate key its storage by `property.name` into a map, so each property gets its own slot. For thread safety, the per-instance field is not synchronized; use `@Volatile`, an `AtomicReference`, or synchronization if multiple threads access the property. Also beware capturing mutable owner state — prefer reading owner state through `thisRef` rather than caching it.
code
kotlin · 14 lines// WRONG: one shared instance, single field -> a and b alias
val shared = object : ReadWriteProperty<Any?, Int> {
private var v = 0
override fun getValue(t: Any?, p: KProperty<*>) = v
override fun setValue(t: Any?, p: KProperty<*>, value: Int) { v = value }
}
class Wrong { var a: Int by shared; var b: Int by shared } // b changes when a does
// RIGHT: fresh instance per property
class Box(private var v: Int = 0) : ReadWriteProperty<Any?, Int> {
override fun getValue(t: Any?, p: KProperty<*>) = v
override fun setValue(t: Any?, p: KProperty<*>, value: Int) { v = value }
}
class Right { var a: Int by Box(); var b: Int by Box() }go deeper
Recognizes that sharing one delegate field across properties causes them to overwrite each other.
Explains per-property fresh instances vs. keying shared storage by property.name.
Adds thread-safety (@Volatile/AtomicReference) and the per-owner keying nuance with thisRef.
Designs a robust reusable delegate API, reasons about identity/aliasing, concurrency, and side-effect discipline in getValue/setValue.
## The bug: shared single-slot delegate A delegate is an ordinary object; its fields hold state. Consider: ```kotlin class Box<T>(private var value: T) : ReadWriteProperty<Any?, T> { override fun getValue(thisRef: Any?, property: KProperty<*>) = value override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value } } val shared = Box(0) class Wrong { var a: Int by shared // same instance var b: Int by shared // same instance } ``` Now `a` and `b` share the single `value` field: setting `a = 1` also makes `b == 1`. The properties **alias** the same storage. ## Why it usually works in practice The idiomatic form `var a by Box(0)` constructs a **fresh** `Box` per property, so each property has its own field — no aliasing. The bug appears only when you deliberately reuse one instance or when `provideDelegate`-style factories return a cached object. (provideDelegate itself is out of scope here; the point is per-property instance identity.) ## Correct designs for a *shared* delegate If you genuinely want one delegate object serving many properties (e.g. a config bag), key the storage by `property.name`: ```kotlin class Bag : ReadWriteProperty<Any?, Any?> { private val store = mutableMapOf<String, Any?>() override fun getValue(thisRef: Any?, property: KProperty<*>) = store[property.name] override fun setValue(thisRef: Any?, property: KProperty<*>, value: Any?) { store[property.name] = value } } ``` Now `a` and `b` map to distinct keys `"a"`/`"b"`. (This is essentially what map-backed delegation does.) But note: this `Bag` instance still can't be shared across *owner instances* without their entries colliding unless you also key by `thisRef` — for per-owner-per-property state you'd key on the pair, or just use a fresh delegate per property as above. ## Thread safety The plain field in `Box` is not safe under concurrency: - Use `@Volatile` for visibility of a reference/primitive, or - `java.util.concurrent.atomic.AtomicReference<T>` for atomic compare-and-set, or - a lock if get-then-set must be atomic. ```kotlin class SafeBox<T>(initial: T) : ReadWriteProperty<Any?, T> { private val ref = java.util.concurrent.atomic.AtomicReference(initial) override fun getValue(thisRef: Any?, property: KProperty<*>): T = ref.get() override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { ref.set(value) } } ``` ## Design checklist - Default to **one delegate instance per property** (the natural `by Factory()` form). - Share an instance only if it keys state by `property.name` (and `thisRef` if per-owner). - Don't cache `thisRef`; read owner state on each call. - Add `@Volatile`/atomics if the property is accessed concurrently. - Keep delegates side-effect-light; getValue should be cheap and ideally idempotent.
- Why does `var a by Box(); var b by Box()` not have the aliasing bug?Each `by Box()` evaluates the constructor separately, producing two distinct delegate instances with independent fields, so the properties don't share storage.
- If one delegate instance is shared across many owner objects, what must the storage key include?Both the property (via property.name) and the owner identity (thisRef), otherwise different owners' values collide; or simply avoid sharing and use a per-property delegate.
One delegate with a single field shared by many properties is like several name tags all pointing at the same locker — everyone's stuff lands in the same place.
saying these in an interview costs you the question
- Not realizing a shared single-field delegate aliases storage across properties
- Thinking `by Box()` reuses one instance for all properties
- Ignoring thread safety of the delegate's mutable field
- Caching thisRef inside the delegate instead of reading it per call
- Assuming map-keyed-by-name handles multiple owners without keying on thisRef too