I want to write an extension method for a list tha...
# getting-started
a
I want to write an extension method for a list that returns a new list with any null items replaced by a default value. I'm having trouble with the generics. There is one generic for the list and there is another generic for the replacement item. How can I specify this relationship between the two generics? I think this is close:
fun <C: Any, T: C?> List<T>.nonNull(sub: C) = map { it ?: sub }
. However, the compiler can't infer the type of C if there is a null in the list. For example, this doesn't compile:
val x = listOf("x", null).nonNull("y")
. You have to explicitly specify the types:
val x = listOf("x", null).nonNull<String, String?>("y")
. Is there something wrong with my method signature that prevents the compiler from inferring the types?
s
Let me see if I can write an equivalent method
Copy code
fun <T : Any, R : T> List<T?>.replaceNulls(sub: R) = map { it ?: sub }
Should be
<T : C>
and
List<T?>
, not
<T : C?>
and
List<T>
a
Yep, that works -- in fact, you can get rid of the R type altogether
s
Oh, neat
a
What if rather than a function, this was encapsulated in an interface and class. So I want to be able to get a properly typed list from the obj and I want to be able to get an item in the list, substituting a default value for a null value. Is there way I can write the interface/class so I don't have to specify the types for c2?
Copy code
interface I<C : Any, T : C?> {
    val list: List<T>
    fun indexAt(index: Int): C
}

class C<C : Any, T : C?>(override val list: List<T>, private val defaultValue: C) : I<C, T> {
    override fun indexAt(index: Int): C = list[index] ?: defaultValue
}

val c1 = C(listOf("a"), "z")
val list: List<String> = c1.list
val c2 = C<String, String?>(listOf("a", null), "z")
val s: List<String?> = c2.list
s
Do you want this class to replace any null values with the default value? Because if so, the sample code wouldn't do that regardless
Copy code
interface Interface<T : Any> {
    val list: List<T>
}

class Class<T : Any>(originalList: List<T?>, private val defaultValue: T) : Interface<T> {
    override val list = originalList.map { it ?: defaultValue }
}

val c1 = Class(listOf("a"), "z")
val list: List<String> = c1.list
val c2 = Class(listOf("a", null), "z")
val s: List<String> = c2.list
a
What I would like is for the original list to be returned for the list property -- no substitution when you get the whole list. The substitution only happens when you use the
indexAt
method. So the type for s should be
List<String?>
not
List<String>
.
a
if you remove the second generic type, kotlin can infer the generic type when constructing a value of type
C
s
Copy code
interface Interface<T : Any> {
    val list: List<T?>
}

class Class<T : Any>(override val list: List<T?>, private val defaultValue: T) : Interface<T> {
    override val list = originalList.map { it ?: defaultValue }
    fun indexAt(index: Int) = list[index] ?: defaultValue
}

val c1 = Class(listOf("a"), "z")
val list = c1.list
val c2 = Class(listOf("a", null), "z")
val s = c2.list
a
but the type of c1.list is
List<String?>
, right? It should be
List<String>
. I want the type of Class.list to be the type of the list that is passed in to the constructor.
s
It doesn’t work like that. If you have the option to pass in either
List<T>
or
List<T?>
, the variable will be stored as
List<T?>
.
a
That's why my original example had two generic parameters: C, which extends Any, and T which is either C or C? The issue is that the compiler can't seem to properly infer the T parameter -- it needs to be explicitly specified. I don't quite know why...
s
Because you can't have a coercing return to either List<T> or List<T?>. It has to be one or the other.
You're asking for something that can't be done within the bounds of this language.