Derek Peirce
02/10/2020, 2:25 AM@Target(AnnotationTarget.PROPERTY)
annotation class Special
class Example(
@Special
val x: String,
@Special
val y: String,
@Special
val z: String
) {
val specials: List<String>
get() = TODO()
}
We could write a method that uses reflection to get and invoke all properties with a particular annotation on a class:
inline fun <reified T : Any> getSpecials(holder: T): List<String> {
return T::class.memberProperties
.filter { it.hasAnnotation<Special>() }
.map { property -> property.call(holder) as String }
}
class Example(
@Special
val x: String,
@Special
val y: String,
@Special
val z: String
) {
val specials: List<String>
get() = getSpecials(this)
}
However, this requires reflection, and the bytecode and execution time will be incredibly large compared to the desired goal of:
val specials: List<String>
get() = listOf(x, y, z)
This could be achieved with the help of an annotation processor, assuming the values aren’t private, but there’s often a large engineering overhead when adding an annotation processor, especially if the code is edited with an IDE and the code isn’t always fully compiled.
However, what if the Kotlin compiler were able to inline the entire concept of properties? The inlined code would look like this:
val specials: List<String>
get() = Example::class.memberProperties
.filter { it.hasAnnotation<Special>() }
.map { property -> property.call(this) as String }
Now, suppose that Kotlin could inline `memberProperties`:
val specials: List<String>
get() = inlineListOf(Example::x, Example::y, Example::z, Example::specials)
.filter { it.hasAnnotation<Special>() }
.map { property -> property.call(this) as String }
This would require Kotlin to have a concept of an inlined list, that automatically applies loop unrolling in the next compilation step:
val specials: List<String>
get() = listOfNotNull(
if (Example::x.hasAnnotation<Special>()) Example::x.call(this) as String else null,
if (Example::y.hasAnnotation<Special>()) Example::y.call(this) as String else null,
if (Example::z.hasAnnotation<Special>()) Example::z.call(this) as String else null,
if (Example::specials.hasAnnotation<Special>()) Example::specials.call(this) as String else null
)
(Using null just for simplicity here. Some other sentinel value and boxing/unboxing would be used to preserve correct values even if the elements are nullable.)
The compiler at this point knows the properties that annotations have, so it would be able to further simplify this to:
val specials: List<String>
get() = listOf(
SpecialHolder::x.call(this) as String,
SpecialHolder::y.call(this) as String,
SpecialHolder::z.call(this) as String
)
At this step, the compiler would identify if any properties got through the filter that weren’t actually strings, so it would become a compilation-time error rather than a runtime error. It wouldn’t catch this for open classes or interfaces, though, those would require a new compilation-time version of as
.
This finally simplifies to:
val specials: List<String>
get() = listOf(x, y, z)