kotlincastinggeneric-listgeneric-type-argument

Unchecked cast : trying to cast Int or String as T (generic type) in the same method


I'm pretty new to generic functions (both in java and kotlin). And I use a function that allows me to restore lists (thanks to SharedPreferences). These lists are either MutableList<Int>, <String>, <Long>, whatever... Here is the code I'm currently using (I saved the list using list.toString(), only if it wasn't empty) :

fun <T: Any> restoreList(sharedPrefsKey: String, list: MutableList<T>) {
    savedGame.getString(sharedPrefsKey, null)?.removeSurrounding("[", "]")?.split(", ")?.forEach { list.add((it.toIntOrNull() ?: it) as T) }
}//"it" is already a String, no need to cast in the "if null" ( ?: ) branch
//warning "Unchecked cast: {Comparable<*> & java.io.Serializable} to T" on "as T"

So my goal is to know how to cast Strings safely to T (the type of the elements inside the list passed as argument). Now I get a warning and want to know if what I'm doing is correct or not. Should I also add an in modifier? For example : list: MutableList<in T> ?


Solution

  • There is no union types in Kotlin. So you can't describe a type T to be either Int or String, hence you can't describe a MutableList<T> to be either MutableList<Int> or MutableList<String>

    But when you do it.toIntOrNull() ?: it you get even not that, but a mutable list, which may contain Int elements as well as a String ones (because compiler have no guarantee that this clause will be resolved same way for each element). So compilers tries to infer this type (which should be a most specific common supertype of both Int and String) and it gets this dreadful Comparable<*> & java.io.Serializable type. This impose so serious restrictions on what T could be, that it become practically useless (it's like using MutableList<*>), and it can't be fixed with variance annotations.

    I would suggest using additional functional parameter here, converting String (after splitting) into an instance of required type (also note that mutating passed parameter inside a function is a code smell, it's better to be merged with existing mutable list in the same scope it was created):

    fun <T> restoreList(sharedPrefsKey: String, converter: (String) -> T): List<T>? =
        savedGame.getString(sharedPrefsKey, null)?.removeSurrounding("[", "]")?.split(", ")?.map { converter(it) }
    

    Usage:

    val listOfInts = restoreList(sharedPrefKey) { it.toIntOrNull() }
    val listOfLongs = restoreList(sharedPrefKey) { it.toLongOrNull() }
    val listOfStrings = restoreList(sharedPrefKey) { it }