Фильтр пустых ключей и значений с карты в Котлине - PullRequest
2 голосов
/ 11 ноября 2019

У меня есть функция расширения, которая отфильтровывает записи с пустыми ключами или значениями:

fun <K, V> Map<K?, V?>.filterNotNull(): Map<K, V> = this.mapNotNull { 
   it.key?.let { key -> 
      it.value?.let { value -> 
         key to value 
      }
   }
}.toMap()

Это работает для карты с обнуляемыми ключами и значениями:

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))

Но не втот, который имеет ненулевые ключи или значения:

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.

Есть ли способ заставить мою функцию расширения работать в обоих случаях, или мне нужно предоставить две отдельные функции?

Ответы [ 3 ]

5 голосов
/ 11 ноября 2019

Я позже выясню почему, но добавление на карту работает:

fun <K : Any, V : Any> Map<out K?, V?>.filterNotNull(): Map<K, V> = ...
0 голосов
/ 15 ноября 2019

Решение

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()

мне кажется слишком сложным. Это может быть записано как

fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
    filter { it.key != null && it.value != null } as Map<K, V>

Уродливое приведение необходимо, так как компилятор не может (пока) сделать вывод, что ни ключи, ни значения не содержат ноль.


Словопредупреждение, касающееся ковариации out K?

Да, он дает возможность использовать один и тот же метод не только для Map<String?, Stuff?>, но и для Map<String, Stuff?> (ключ может обнуляться или нет). Но эта свобода имеет свою цену. Для карт, которые, как известно, не имеют нулевых ключей, вам не нужно платить нулевое сравнение для каждой записи.

В вашем первоначальном решении - что без ковариации на K - компилятор может помешать вам вызвать этот неэффективный метод. Правильный метод тогда, вероятно, будет

fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
0 голосов
/ 11 ноября 2019

Вы можете указать тип карты при использовании mapOf

assert(mapOf<String?, String?>("x" to null, "a" to "b").filterNotNull() == mapOf("a" to "b"))

РЕДАКТИРОВАТЬ

Вы указали функцию расширения для Map<K?, V?> и пытались использоватьэто вывод Map<String, String> (в вашем исходном вопросе), поэтому он не будет работать, поскольку Map<String, String> не является подтипом Map<K?, V?>, потому что интерфейс карты определен как Map<K, out V>. Он инвариантен для типа параметра ключа и ковариантен для типа параметра значения.

Что вы можете сделать, это сделать тип ключа в вашей функции расширения также ковариантным, изменив вместо Map<K?, V?> на Map<out K?, V?>. Теперь Map<String, String> или Map<String, String?> будет подтипом Map<K? V?>.

Вы также можете использовать biLet вместо двух вложенных let: Как я могу проверить 2 условия, используя let (или apply etc)

...