I'm trying to use delegated properties in Kotlin to provide some type safety to application preferences that are persisted to a non-typed store. The store is semanitcally a bit like some type of Map<String,Any>
, although it doesn't implement that specific class.
The problem i have is that it doesn't appear possible for a single delegate class to implement getValue
and setValue
for more than one type.
If this is the type-safe SomePreferences
class that other applications will call to get/set preferenceOne
and preferenceTwo
:
class SomePreferences {
private val myDelegate = MyPreferenceStorageImpl()
var preferenceOne: Boolean by myDelegate
var preferenceTwo: String by myDelegate
}
However, if I try and create MyPreferencesStorageImpl
, it tries to look something like this:
class MyPreferenceStorageImpl() {
operator fun getValue(somePreferences: SomePreferences, property: KProperty<*>): Boolean {
TODO("Read from underlying store, transforming the read value as a boolean")
}
operator fun setValue(somePreferences: SomePreferences, property: KProperty<*>, b: Boolean) {
TODO("Write to underlying store, transforming the value from a Boolean")
}
operator fun getValue(somePreferences: SomePreferences, property: KProperty<*>): String {
TODO("Read from underlying store, transforming value to a String")
}
operator fun setValue(somePreferences: SomePreferences, property: KProperty<*>, s: String) {
TODO("Write to underlying store, transforming given value to a string")
}
}
However, this doesn't work, because I've got two methods who only differ by return type, which is a conflict.
I've a couple of half-baked ideas on how to solve this, including trying to subclass Map<String, Any>
in some way, or maybe provide a type-specific implementation per preference type (ew), or generics (somehow) but these don't feel particularly right.
Am I missing an obviously better approach here?
CodePudding user response:
Assuming that you won't be using the delegate on properties with a type parameter as their type...
You can make it generic:
class MyPreferenceStorageImpl() {
operator fun <T> getValue(somePreferences: SomePreferences, property: KProperty<*>): T {
TODO("Read from underlying store, transforming the read value as a T")
}
operator fun <T> setValue(somePreferences: SomePreferences, property: KProperty<*>, b: T) {
TODO("Write to underlying store, transforming the value from a T")
}
}
Note that when you return from getValue
, you would need to do an unchecked cast. This cannot be 100% safe, because T
could have its own type parameters. You can at least check something like this though:
// suppose you've got the value from storage and it's now stored in "returnValue"
if ((property.returnType.classifier as? KClass<*>)?.isInstance(returnValue) == true) {
return returnValue as T
} else {
// something went wrong, returnValue is not an instance of T
}
If you need to handle each type (String
, Int
, Boolean
) differently, you can do:
when (property.returnType) {
typeOf<String>() -> TODO()
typeOf<Int>() -> TODO()
typeOf<Boolean>() -> TODO()
else -> throw UnsupportedOperationException()
}
CodePudding user response:
If you want to do it without the reflection library, you can use reified types.
class MyPreferenceStorageImpl {
inline operator fun <reified T> getValue(somePreferences: SomePreferences, property: KProperty<*>): T {
when (T::class) {
Boolean::class -> TODO("Read from underlying store, transforming the read value as a Boolean")
String::class -> TODO("Read from underlying store, transforming the read value as a String")
else -> error("${T::class} is an unsupported property type for delegation.")
}
}
inline operator fun <reified T> setValue(somePreferences: SomePreferences, property: KProperty<*>, value: T) {
when (T::class) {
Boolean::class -> TODO("Write to underlying store, transforming the value from a Boolean")
String::class -> TODO("Write to underlying store, transforming the value from a String")
else -> error("${T::class} is an unsupported property type for delegation.")
}
}
}