https://kotlinlang.org logo
m

myaa

05/28/2020, 6:20 PM
how do i construct a class where 1. the fields of the class can be set using named constructor arguments with default values 2. those fields are accessible and settable through a map without having to declare each variable twice (once as a field and once as an argument)? so far i've got the following not-quite-working example
Copy code
class Test(
        first: String = "default",
        second: Int = 42,
        third: Boolean = false
) {
    val map = mutableMapOf<String, Any>()

    inner class MyDelegate(private val default: Any) {
        operator fun getValue(receiver: Test, property: KProperty<*>) =
                map.computeIfAbsent(property.name) { default }

        operator fun setValue(receiver: Test, property: KProperty<*>, value: Any) {
            map[property.name] = value
        }
    }

    var first by MyDelegate(first)
    var second by MyDelegate(second)
    var third by MyDelegate(third)
}

val t = Test(second = 100)
t.first = "test"
println(t.map) // {first=test} - WRONG
println("${t.first} ${t.second} ${t.third}") // test 100 false
but i don't like the redundant definitions (having to declare each field as an argument as well), and this example also behaves incorrectly in that the map doesn't get populated with all the arguments immediately upon instantiation
🤔 1
s

Stefan Beyer

05/28/2020, 6:32 PM
it populates fine for me
sadly, property delegation is not possible with properties defined directly in the constructor 😞
m

myaa

05/28/2020, 6:34 PM
oops sorry, i put the lines in the wrong order. the map only gets populated because the fields were just accessed on the previous line. if you try to access the map without first accessing the fields, you wouldn't see the values
s

Stefan Beyer

05/28/2020, 6:34 PM
oh yes, I see
m

myaa

05/28/2020, 6:36 PM
but yes, exactly, if we could do something like property delegation with properties defined in the constructor that would be just what i need
s

Stefan Beyer

05/28/2020, 6:43 PM
but delegation itself is just re-routing the getters and setters to another object. even when constructor properties could be delegated, the map would still not auto fill
k

keturn

05/28/2020, 6:43 PM
strikes me as the sort of thing that the language itself might not support, but maybe doable with some clever annotation and metaprogramming. that's just a hunch, though.
s

Stefan Beyer

05/28/2020, 6:44 PM
if you want the map filled instantly without repeating them in code, you would need reflection
m

myaa

05/28/2020, 6:46 PM
yeah, i wouldn't mind using a bit of reflection, as long as it means not having to define the variables twice
you know, thinking about it, i suppose the best solution might actually be to have the class itself implement the
Map
interface? and then implement the
get
and
put
methods using reflection
🤔 1
rather than messing around with delegates
s

Stefan Beyer

05/28/2020, 6:54 PM
you could do that bu then you would have a reflection call on every property access... my thought was to make the map lazy and collect everything once it is accessed the first time
m

myaa

05/28/2020, 6:58 PM
yeah that would be ideal, but i'm not if it's possible to ensure that assignments to the fields also update the map without using delegates or custom setters, and neither seem to be possible with constructor arguments
m

Matteo Mirk

05/28/2020, 7:03 PM
I came up with this, but now I realize there’s some repetition on parameter that you wanted to avoid… not quite the thing you were looking for 😕
Copy code
class Test private constructor(val map: MutableMap<String, Any?>) {
    var first by map
    var second by map
    var third by map

    companion object {
        operator fun invoke(
                first: String = "default",
                second: Int = 42,
                third: Boolean = false
        ) = Test(mutableMapOf(
                "first" to first,
                "second" to second,
                "third" to third
        ))
    }
}
s

Stefan Beyer

05/28/2020, 7:13 PM
Copy code
import kotlin.reflect.KProperty
import kotlin.reflect.full.declaredMemberProperties

class Test(
    first: String = "default",
    second: Int = 42,
    third: Boolean = false
) {
    val map: MutableMap<String, Any?> by lazy {
        this::class
            .declaredMemberProperties
            .filter { it.name != "map" }
            .associate { it.name to it.getter.call(this) }
            .toMutableMap()
    }

    inner class MyDelegate<T>(private var currentValue: T) {
        operator fun getValue(receiver: Test, property: KProperty<*>): T = currentValue
        operator fun setValue(receiver: Test, property: KProperty<*>, value: T) {
            currentValue = value
            receiver.map[property.name] = value
        }
    }

    var first by MyDelegate(first)
    var second by MyDelegate(second)
    var third by MyDelegate(third)
}

val t = Test(second = 100)
t.first = "test"
println(t.map) // {first=test, second=100, third=false}
println("${t.first} ${t.second} ${t.third}") // test 100 false
i have moved the "source of truth" into the delegate, because then, the getter has no call to map and we can safely call it from the initialization logic of map without causing a stack overflow ^^
oh and also when you use receiver.map in the setter, you can even move the delegate class outside the test class
m

myaa

05/28/2020, 8:19 PM
thanks for the suggestions, i think i have a few good alternatives to choose between/base my implementation on now. i'll have to think about to what extent i want to balance performance vs redundant declarations in my use case
5 Views