Having trouble with type erasure

Viewed 82

I have something like this :

import kotlin.reflect.KClass

class Quantity<T> { 
    /* ... */ 
}

class Field<T : Any> { 
    val type: KClass<T> get() = TODO("This is initialized, don't worry about implentation details, just know that fields know their type.")

    fun initValue(value: T) {
        /* Do something very useful */
    }

    /* Other methods */

    class Template<T : Any> {
        fun initFieldWithValue(value: T): Field<T> {
            return Field<T>().apply {
                this.initValue(value)
            }
        }
    }
}


class ComponentClass(
    val fieldsTemplates: Map<String, Field.Template<*>>
) {
    inner class Instance(field: Map<String, Field<*>>)

    fun new(fieldValues: Map<String, Quantity<*>>): Instance {
        val fields = mutableMapOf<String, Field<*>>()
        for ((fieldName, template) in fieldsTemplates) {
            fields[fieldName] = fieldsTemplates
                .getValue(fieldName)    
                .initFieldWithValue(fieldValues.getValue(fieldName) /* Here a type error */)
        }
        return Instance(fields)
    }
}

As you might guess, this is intended to work as a 'runtime way' of creating classes that own fields (Field<T> class), each one possessing a typed value (represented by a Quantity<T>).

The problem is that this code won't compile due to the fact that the quantity retrieved from fieldValues when creating the different fields of the future Instance in the new method isn't guaranteed to be of the required type for the field it is stuffed into.

The problem is that I would need a check since filling a Field<Quantity<String>> with a Quantity<Int> is obviously not a good idea, but because of the type erasure I cannot ensure that the quantities passed in are of the good type.

Any idea ? One more thought : Fields know what their type is thanks to their type attribute, but unfortunately I can't do the same for the Quantity class...

1 Answers

Your initFieldWithValue function is enforcing the type of the parameter to match the type known by the Template/Field. But inside your new function, your Template is a Template<*> since you retrieve it from a collection where the values are of this type.

The point of generics is to enforce compile time checks so casting can be done safely and automatically under the hood. This is only useful when your type is known at compile time. In this case, the type is not known at compile time, so the generics are preventing your code from compiling. This is what generics are supposed to do: prevent code from compiling if the compiler cannot check that they types match.

If you want this code to compile, you should change initFieldWithValue so it doesn't enforce generics. You can instead manually check the type and throw an error or exit early if it's incorrect. It will be up to your code elsewhere to ensure you aren't mixing and matching types.

Here's an example of a version that would work. The type check it does requires the Kotlin reflection library. If you're targeting JVM only, you can use the Java Class.isAssignableFrom method instead to do this check.

class Template<T : Any> {
    val type: KClass<T> get() = TODO()

    /**
     * @throws IllegalStateException if [value] is not of the same type 
     * as this Template's [type]. 
     */
    fun initFieldWithValue(value: Any): Field<T> {
        if (!value::class.isSubclassOf(type)) {
            error("Invalid value type for Field type of $type")
        }
        return Field<T>().apply {
            @Suppress("UNCHECKED_CAST") // we manually checked it above
            initValue(value as T)
        }
    }
}
Related