Function return type is <out T> instead of expected <T>

The “getPreferenceKey” function (below) return type is “Preferences.Key<T>”. However, when I try to assign the return value into “prefKey” of type “Preferences.Key<T>” I get a compile time error saying that the actual return type of “getPreferenceKey” is “Preferences.Key<out T>” for some reason.

How come it’s <out T>? Am I missing something here?

val prefKey: Preferences.Key<T> = getPreferenceKey(key, value::class)
private fun <T : Any> getPreferenceKey(key: String, kClass: KClass<T>): Preferences.Key<T> {
        return when (kClass) {
            String::class -> stringPreferencesKey(key)
            Boolean::class -> booleanPreferencesKey(key)
            Int::class -> intPreferencesKey(key)
            Long::class -> longPreferencesKey(key)
            Float::class -> floatPreferencesKey(key)
            Double::class -> doublePreferencesKey(key)
            else -> throw IllegalArgumentException("Key-value store does not support the type passed")
        } as Preferences.Key<T>
    }

I think it’s because a KClass instance obtained from ::class is out-projected by default. One approach you could take is:

private fun <T : Any> getPreferenceKey(key: String, kClass: KClass<out T>): Preferences.Key<T> {

with the rest of the function body the same

You are right. Thank you!

To give you additional context: this code is generally not entirely correct from the type safety perspective. It is fine in your specific case, because you support very few types only, but if you write a similar code for other types, it might cause class cast exceptions at runtime. This is why the compiler complains about it.

1 Like

In which situation could it create a CCE?

Simple, artificial example would be:

fun main() {
    addToList(Dog(), Cat())
    dogs.forEach {} // CCE: Cat cannot be cast to Dog
}

fun addToList(animal1: Animal, animal2: Animal) {
    val list = getList(animal1::class)
    list += animal1
    list += animal2
}

private fun <T : Any> getList(kClass: KClass<out T>): MutableList<T> {
    return when (kClass) {
        Animal::class -> animals
        Dog::class -> dogs
        else -> throw IllegalArgumentException()
    } as MutableList<T>
}

private val animals = mutableListOf<Animal>()
private val dogs = mutableListOf<Dog>()

open class Animal
class Dog : Animal()
class Cat : Animal()

Let’s ignore why we get a list for the animal1 and then we add animal2 to it. The point is: we added a cat to a list of dogs and compiler allowed us to do this.

The root cause is that animal1::class acquires the runtime type. animal1 is typed as Animal, but animal1::class could return KClass<Animal>, KClass<Dog> or KClass<Cat> - we don’t know, and we can’t pretend it is just KClass<Animal>. This is why out.

Because animal1 is typed as Animal, getList says it returns MutableList<Animal>. In fact, it returns MutableList<Dog>.

As said earlier, this problem doesn’t exist in the specific case of OP, because the list of allowed types is constrained to just a few of them.

edit:
I believe the original code was type-safe. animal1::class returns KClass<out Animal> - meaning we don’t know what’s the exact type. Then getList returns MutableList<out Animal>, so again, we don’t know the exact type of the list, and we can’t add anything to it. After we add out as suggested above, the unchecked cast in as MutableList<T> is no longer type-safe.

2 Likes