Inline value classes are really useful as type-safe identifiers. However, sometimes you need to implement a generic conversion between the wrapper, and the naked type (which is actually conceptually no-op, when the wrapper is inlined and not boxed).
Some examples include - marshalling such IDs to/from json, (de)-serialization to database etc.
The conversion from wrapper to naked type is easy. Just let wrappers implement some common interface that exposes the value of the wrapper.
interface Id<T : Serializable> {
val id: T
}
@JvmInline
value class UserId(override val id: String) : Id<String>
To go from wrapper to value, just access the id value.
But how do you convert the value to the wrapper?
You cannot even use introspection, which you could if the wrapper would be implemented as a real wrapper. Then you could instantiate the wrapper using introspection, if the type of the wrapper is known. But if you want to convert a value to unboxed wrapper, no instantiation actually happens, does it?
Is there any way around this problem? The only workaround I see is to create a nasty switch where essentially all known wrapper types need to be registered:
@Suppress("UNCHECKED_CAST")
fun <T : Serializable, I : Id<T>> wrap(type : KClass<I>, value : T): I = when (type) {
UserId::class -> UserId(value as String) as I
ProjectId::class -> ProjectId(value as String) as I
else -> throw IllegalArgumentException("Unsupported type $type")
}
Is there some way to automate this conversion?
I also do not understand how the unboxed state of an instance actually works, when any instance, even in a context in which it can be unboxed, knows it correct, i.e. wrapper, type, as specified in documentation: Inline value classes | Kotlin Documentation
If I modify the code as:
fun asInline(f: Foo) {
println(f)
}
it prints Foo(i=42)
, how could this work, without invoking the actual constructor of the type Foo?
Maybe some reification magic happens here, which allows calling wrapper’s methods with naked instance, including .toString
and .javaType
.