Reputation: 16575
I have an extension function that filters away entries with null keys or values:
fun <K, V> Map<K?, V?>.filterNotNull(): Map<K, V> = this.mapNotNull {
it.key?.let { key ->
it.value?.let { value ->
key to value
}
}
}.toMap()
This works for a map with nullable keys and values:
data class Stuff(val foo: Int)
val nullMap = mapOf<String?, Stuff?>(null to (Stuff(1)), "b" to null, "c" to Stuff(3))
assert(nullMap.filterNotNull().map { it.value.foo } == listOf(3))
But not in one that has non-nullable keys or values:
val nullValues = mapOf<String, Stuff?>("a" to null, "b" to Stuff(3))
assert(nullValues.filterNotNull().map { it.value.foo } == listOf(3))
Type mismatch: inferred type is Map<String, Stuff?> but Map<String?, Stuff?> was expected
Type inference failed. Please try to specify type arguments explicitly.
Is there a way to make my extension function work for both cases, or do I need to provide two separate functions?
Upvotes: 4
Views: 9871
Reputation: 21
As improvement to the nested let
s, you can use:
private fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
mapNotNull { (k, v) ->
when {
k == null -> null
v == null -> null
else -> k to v
}
}.toMap()
which can be condensed to
private fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
mapNotNull { (k, v) -> if (k == null || v == null) null else k to v }.toMap()
Upvotes: 2
Reputation: 2083
You can specify the Map type when using mapOf
assert(mapOf<String?, String?>("x" to null, "a" to "b").filterNotNull() == mapOf("a" to "b"))
EDIT
You specified the extension function for Map<K?, V?>
and was trying to use it an inferred Map<String, String>
(in your original question), so it wouldn't work as Map<String, String>
is not a subtype of Map<K?, V?>
, cause map interface is defined as Map<K, out V>
. It is invariant on the key parameter type and covariant on the value parameter type.
What you can do is to make the key type in your extension function also covariant by changing Map<K?, V?>
to Map<out K?, V?>
instead. Now, Map<String, String>
or Map<String, String?>
will be a subtype of Map<K? V?>
.
You can also use a biLet instead two nested let: How can I check 2 conditions using let (or apply etc)
Upvotes: 2
Reputation: 3165
The solution
fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> = this.mapNotNull {
it.key?.let { key ->
it.value?.let { value ->
key to value
}
}
}.toMap()
seems overcomplicated to me. It could be written as
fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
filter { it.key != null && it.value != null } as Map<K, V>
The ugly cast is necessary, since the compiler can not (yet) deduce, that neither keys nor values contain null.
A word of warning, concerning the covariance out K?
Yes, it offers the possibility to use the same method not only for Map<String?, Stuff?>
but also for Map<String, Stuff?>
(key nullable or not). But this freedom comes with a cost. For maps, known to have no null keys, you needlessly pay the null comparison for every entry.
In your initial solution - that without covariance on K -, the compiler could prevent you from calling that inefficient method. The proper method then probably is
fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
Upvotes: 3
Reputation: 337
I will later figure out why, but adding out to the map is working:
fun <K : Any, V : Any> Map<out K?, V?>.filterNotNull(): Map<K, V> = ...
Upvotes: 7