Home > Software engineering >  How to implement property delegate that works with an arbitrary type
How to implement property delegate that works with an arbitrary type

Time:01-06

I'm trying to implement custom property delegate to store props in a Map (just like in docs, for the educational purposes)

Delegate implementation must:

  1. allow to store properties of arbitrary type
  2. allow for type inference (just like the original one)

I've wrote a code that satisfies first condition:

import kotlin.reflect.KProperty

fun main() {
    val e = Example(PropInMapDelegate())
    e.myProp = 1;
    println(e.myProp) // e.myProp is "Any?" :(
}

class Example(propInMapDelegate: PropInMapDelegate) {
    var myProp by propInMapDelegate
}

class PropInMapDelegate {
    private val _map: HashMap<String, Any?> = HashMap()

    operator fun getValue(thisRef: Any, property: KProperty<*>): Any? {
        return _map[property.name]
    }

    operator fun setValue(thisRef: Any, property: KProperty<*>, value: Any?) {
        _map[property.name] = value
    }

}

But, it has obvious 2 issues:

  1. Using "Any?" breaks type inference
  2. Explicitly specifying property type var myProp: Int by propInMapDelegate leads to errors:

error#1

Property delegate must have a 'getValue(Example, KProperty*>)' method. None of the following functions are suitable.
getValue(Any?, KProperty<*>) defined in PropInMapDelegate

error#2

Property delegate must have a 'setValue(Example, KProperty*>, Int)' method. None of the following functions are suitable.
setValue(Any?, KProperty<*>, Any?) defined in PropInMapDelegate

I've tried to employ generics to allow for type inference, but failed.

Changing PropInMapDelegate implementation like so:

class PropInMapDelegate {
    private val _map: HashMap<String, Any?> = HashMap()

    operator fun <T>getValue(thisRef: Any, property: KProperty<*>): T? {
        return _map[property.name] as T // That feels very wrong
    }

    operator fun <T>setValue(thisRef: Any, property: KProperty<*>, value: T?) {
        _map[property.name] = value
    }

}

And using the following

class Example(propInMapDelegate: PropInMapDelegate) {
    var myProp: Int by propInMapDelegate // error (see below)
}

Leads to: error#1

Property delegate must have a 'getValue(Example, KProperty*>)' method. None of the following functions are suitable.
getValue(Any, KProperty<*>)   where T = Int for    operator fun <T> getValue(thisRef: Any, property: KProperty<*>): T? defined in PropInMapDelegate

error#2

Property delegate must have a 'setValue(Example, KProperty*>, Int)' method. None of the following functions are suitable.
setValue(Any, KProperty<*>, Int?)   where T = Int for    operator fun <T> setValue(thisRef: Any, property: KProperty<*>, value: T?): Unit defined in PropInMapDelegate

Questions:

  1. What am I missing?
  2. Why does Kotlin expect the exact "Example" type in setValue/getValue signature?
  3. How to implement "getValue" without using unsafe "as T" operator? Edit: it seems that original implementation utilizes as too, so it's probably ok?
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
    @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1)

CodePudding user response:

I think the error message is misleading you because of the way it is worded. "Example" isn't the part that doesn't match. Since Example is a subtype of Any, that part of your getValue() function signature is fine.

The part that doesn't match is that you are returning T? but you're trying to use it for a non-nullable property.

If you change your property to be nullable, it will work:

var myProp: Int? by propInMapDelegate

Or if you change your operator function to return a non-nullable, it will work:

operator fun <T> getValue(thisRef: Any, property: KProperty<*>): T {
    return _map[property.name] as T
}

Note that casting as T will fail if the value for that key doesn't exist in the map and you're using this for a non-nullable property, because null cannot be cast to a non-nullable T.

Regarding your question marked 3: As you can see in the standard library source code, casting is unavoidable if you are using a map of arbitrary value types. It's part of the potential danger in using a map for this purpose. It is also possible the map doesn't have the values at all that match up with the requested property names.

  • Related