Extension Variables with state in Kotlin
Published on
Why doesn't this work?
class Foo {}
var Foo.bar: String = "hello world"
The code above will give you an error about the extension property not having a backing field. Essentially, all extensions are static methods. Since the variable being declared isn't actually part of the class, it can't store our variable as part of the class. This makes sense, but is there any way around it?
What you're meant to use extension variables for is static getter/setters. Something like this:
class Foo {
var name: String? = null
}
var Foo.bar: String
get() = name ?: "N/A"
set(value) { name = value }
That works great if you want to create some sort of facade or derived property based on the class's existing data. But it can't be used to store new data.
The extension has access to public properties on the extended object, but nothing private. Just like a static method. This is why it can't store any state on the class, we're not actually changing the parent object, we're just defining a global static method.
Crimes and Hashmaps
If you want to store data per-class as an extension, it can be tricky. But we can get something similar working by storing the data elsewhere, like in a map associated with the class instance.
Ex:
private val fooBars = mutableMapOf<Foo, String>()
var Foo.bar: String
get() = fooBars[this] ?: "N/A"
set(value) { fooBars[this] = value }
That works great! We've made a synthetic extension variable on our class! No problem.
.. Okay, one problem: the code above creates a memory leak. Since the
code above is a statically defined global variable that holds a reference
to the Foo
object, the garbage collector will never collect it! If your
application is short lived, or uses only a few of these objects, this could
be fine to ignore. But this is why this isn't a first-class feature in Kotlin.
We can improve the above code slightly by keeping an indirect reference
to the Foo
class. Since Kotlin Multiplatform doesn't have a WeakReference
object, let's look at how to do that with just a hashcode:
Ex:
private val fooBars = mutableMapOf<Int, String>()
var Foo.bar: String
get() = fooBars[hashCode()] ?: "N/A"
set(value) { fooBars[hashCode()] = value }
By changing the key to an Int
and using the object's hashcode, the garbage
collector can now destroy the Foo
object when appropriate. But this still
leaves the tombstone in our map, along with the data we're storing. In this
example it's just storing a String
, but if that value was something more
complex, it would be a similar memory leak still.
If you're only running in the JVM, you can use a WeakHashMap
to solve
this problem, and the values will eventually get garbage collected as well
Ex:
private val fooBars = WeakHashMap<Foo, String>()
var Foo.bar: String
get() = fooBars[this] ?: "N/A"
set(value) { fooBars[this] = value }
If Kotlin ever gets a proper WeakReference
implementation in common code,
the above solution would work well on all platforms.