I’m migrating some code to Kotlin 1.5, and I’d really like to use inline classes for some very widely used library classes. However, these are used by a lot of Java code, and while ABI compatibility is not an issue, source compatibility is. These classes usually follow a kind of “sealed private data class” pattern to avoid the public copy()
issue:
sealed class MyValueClass {
abstract val value: Int
fun acceptInt(i: Int) { }
fun acceptMyClass(other: MyValueClass) { }
fun returnMyClass(): MyValueClass = this
companion object {
@JvmStatic fun of(value: Int): MyValueClass = MyValueClassImpl(value)
@JvmField val ZERO: MyValueClass = MyValueClassImpl(0)
}
private data class MyValueClassImpl(override val value: Int): MyValueClass()
}
While this isn’t as elegant as it could be if I could simply use a data class without a copy()
function, it works just perfectly. Now when I’m trying to make it an inline class, all kinds of things go wrong as far as Java interop is concerned.
@JvmInline
value class MyValueClass private constructor(val value: Int) {
fun acceptInt(i: Int) { }
fun acceptMyClass(other: MyValueClass) { }
fun returnMyClass(): MyValueClass = this
companion object {
@JvmStatic fun of(value: Int): MyValueClass = MyValueClass(value)
@JvmField val ZERO: MyValueClass = MyValueClass(0)
}
}
What happens is:
- the
acceptInt
function is compiled into a static function accepting twoint
s (this
andi
); - the same goes for
acceptMyClass
, asother
is anint
too; - the
returnMyClass
function returns anint
; - the
of
function returns anint
too; - the
ZERO
constant doesn’t even compile because a@JvmField
can’t be of an inline class type.
And with the name mangling I can’t even use most (all?) of these functions from Java even if I was willing to work with int
s directly (thereby turning all this stuff into procedural type-unsafe programming).
What could be really useful here is to somehow get it work transparently with Java, even if at the cost of autoboxing. But I see no magic annotation or compiler option for that. What I really want to have is an unmangled boxed counterpart for every function, so that of
returns a MyValueClass
instance when called from Java, and an Int
when called from Kotlin.
I actually managed to get it done with some hardcore interface abuse:
sealed interface MyValueClass {
val value: Int
fun acceptInt(i: Int)
fun acceptMyClass(other: MyValueClass)
fun returnMyClass(): MyValueClass
companion object {
@JvmStatic fun of(value: Int): MyValueClass = MyValueClassImpl.of(value)
@JvmField val ZERO: MyValueClass = MyValueClassImpl.ZERO
}
}
@JvmInline
value class MyValueClassImpl(override val value: Int): MyValueClass {
override fun acceptInt(i: Int) { }
override fun acceptMyClass(other: MyValueClass) = acceptMyClass(other as MyValueClassImpl)
fun acceptMyClass(other: MyValueClassImpl) { }
override fun returnMyClass() = this
companion object {
fun of(value: Int) = MyValueClassImpl(value)
val ZERO = MyValueClassImpl(0)
}
}
This isn’t as ugly as using int
s directly from Java would be, but it’s still far from perfect. Not only it’s too verbose (which is an acceptable price for an optimization), but the need to use MyValueClassImpl
everywhere in Kotlin is very annoying (and makes converting Java code to Kotlin code harder). This is an acceptable price for an optimization as well, but is on the verge of being too complicated.
What’s especially frustrating is that Kotlin already generates a boxing class for a value class. All it needs to do is to add all members to it (with companion object members, I can live with special overloads for Java). But instead of implementing member functions as true members, it simply unboxes this
on every call, and then passes the unboxed value to the static implementation, that is mangled and inaccessible from Java (and is useless anyway even if I unmangle it with @JvmName
).