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
acceptIntfunction is compiled into a static function accepting twoints (thisandi); - the same goes for
acceptMyClass, asotheris aninttoo; - the
returnMyClassfunction returns anint; - the
offunction returns aninttoo; - the
ZEROconstant doesn’t even compile because a@JvmFieldcan’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 ints 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 ints 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).