Reification is a powerful concept in Kotlin that allows us to retain generic type information at runtime. However, it comes with a significant limitation: it only works for inline functions. But why is that the case? Let’s explore the reasons behind this restriction and understand how reification truly works.
Understanding Reification
In most JVM-based languages, including Kotlin and Java, generic type parameters are erased at runtime due to type erasure. This means that when a function or class uses generics, the type information is not available at runtime. For example, the following function:
fun <T> printType(value: T) {
println(value::class) // Error: Type information is erased
}
The above code won’t work as expected because T
is erased and does not retain type information.
How Reification Works
Reification in Kotlin allows us to retain generic type information at runtime when using inline functions. It enables us to work with generics in a way that would otherwise be impossible due to type erasure.
To make a generic type reified, we use the reified
keyword inside an inline
function:
inline fun <reified T> printType(item: T) {
println(T::class) // Works because T is reified
}
Now, if we call:
printType("Hello")
The output will be:
class kotlin.String
Unlike the earlier example, this works because T
is no longer erased. But why does this work only for inline functions?
Why reification works for inline functions only?
Reification works for inline functions because the compiler inserts the bytecode implementing the inline function directly at every place where it is called. This means that the compiler knows the exact type used as the type argument in each specific call to the inline function.
Let’s understand this concept with the filterIsInstance()
function from the Kotlin standard library.
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
When you call an inline function with a reified type parameter, the compiler can generate a bytecode that references the specific class used as the type argument for that particular call. For example, in the case of the filterIsInstance<String>()
call, the generated code would be equivalent to:
for (element in this) {
if (element is String) {
destination.add(element)
}
}
The generated bytecode references the specific String
class, not a type parameter, so it is not affected by the type-argument erasure that occurs at runtime. This allows the reified type parameter to be used for type checks and other operations at runtime.
What Happens if You Try Reification in a Non-Inline Function?
If you try to use a reified type parameter in a non-inline function, you’ll get a compilation error:
fun <reified T> printNonInlineType(value: T) { // Error
println(T::class)
}
Error:
Error: Only type parameters of inline functions can be reified or Reified type parameters can only be used in inline functions
This error occurs because, without inlining, the type information would be erased, making T::class
invalid.
Workarounds for Non-Inline Functions
If you need to retain type information in a non-inline function, consider using class references or passing a KClass<T>
parameter:
fun <T: Any> printType(clazz: KClass<T>, value: T) {
println(clazz)
}
printType(String::class, "Hello")
This approach ensures the type is explicitly provided and prevents type erasure.
Conclusion
Reification is a powerful feature in Kotlin, but it is only possible within inline functions due to JVM type erasure. Inline functions allow type parameters to be substituted at compile time, preserving the type information at runtime. If you need to work with generic types in non-inline functions, you’ll need alternative solutions like KClass
references.
Understanding this limitation helps developers write more effective and optimized Kotlin code while leveraging the benefits of reification where necessary.