У меня есть два списка: один со старыми данными, где следует сохранить Boolean, и новые данные, которые следует объединить со старыми данными. Лучше всего это можно увидеть в этом модульном тесте:
@Test
fun mergeNewDataWithOld() {
// dog names can be treated as unique IDs here
data class Dog(val id: String, val owner: String)
val dogsAreCute: List<Pair<Dog, Boolean>> = listOf(
Dog("Kessi", "Marc") to true,
Dog("Rocky", "Martin") to false,
Dog("Molly", "Martin") to true
)
// loaded by the backend, so can contain new data
val newDogs: List<Dog> = listOf(
Dog("Kessi", "Marc"),
Dog("Rocky", "Marc"),
Dog("Buddy", "Martin")
)
// this should be the result: an intersection that preserves the extra Boolean,
// but replaces dogs by their new updated data
val expected = listOf(
newDogs[0] to true,
newDogs[1] to false
)
// HERE: this is the code I use to get the expected union that should contain
// the `Boolean` value of the old list, but all new `Dog` instances by the new list:
val oldDogsMap = dogsAreCute.associate { it.first.id to it }
val newDogsMap = newDogs.associateBy { it.id }
val actual = oldDogsMap
.filterKeys { newDogsMap.containsKey(it) }
.map { newDogsMap[it.key]!! to it.value.second }
assertEquals(expected, actual)
}
Мой вопрос: как лучше написать код для получения моей переменной actual? Мне особенно не нравится, что я сначала фильтрую ключи, содержащиеся в списках оба, но затем мне приходится явно использовать newDogsMap[it.key]!!, чтобы получить нулевые безопасные значения.
Как я могу это улучшить?
Обновлено благодаря Марко: я хочу сделать перекресток, а не объединение. Легко сделать пересечение списков:
val list1 = listOf(1, 2, 3)
val list2 = listOf(4, 3, 2)
list1.intersect(list2)
// [2, 3]
Но на самом деле мне нужно пересечение на картах:
val map1 = mapOf(1 to true, 2 to false, 3 to true)
val map2 = mapOf(4 to "four", 3 to "three", 2 to "two")
// TODO: how to do get the intersection of maps?
// For example something like:
// [2 to Pair(false, "two"), 3 to Pair(true, "three")]
Вы правы - я хочу сделать перекресток. Но мне нужно пересечение на картах - насколько я знаю, Kotlin поддерживает пересечение только в списках.
Если обе ваши карты содержат одинаковые типы значений, вы можете использовать merge, если у вас есть MutableMap. Если вы этого не сделали и не хотите работать с null-safe-операторами, то я не знаю более простого подхода, чем показанный. Вместо этого он просто усложняется, или вы пишете больше кода, и я не думаю, что он станет более читаемым, если опустить другой null-safe-operator ;-)





Ну вот:
val actual = oldDogsMap.flatMap { oDEntry ->
newDogsMap.filterKeys { oDEntry.key == it }
.map { it.value to oDEntry.value.second }
}
Обратите внимание, что я сконцентрировался только на том, «как не использовать здесь !!» ;-)
Или, конечно, работает и наоборот:
val actual = newDogsMap.flatMap { nDE ->
oldDogsMap.filterKeys { nDE.key == it }
.map { nDE.value to it.value.second }
}
Вам просто нужно иметь соответствующий внешний вход, и вы (null-) в безопасности.
Таким образом вы избавитесь от всех этих операций, безопасных для null (например, !!, ?., mapNotNull, firstOrNull() и т. д.).
Другой подход - добавить cute как свойство к data class Dog и вместо этого использовать MutableMap для новых собак. Таким образом, вы можете соответствующим образом merge значений, используя вашу собственную функцию слияния. Но, как вы сказали в комментариях, вам не нужен MutableMap, так что тогда это не сработает.
Если вам не нравится то, что здесь происходит, и вы хотите скрыть это от кого-либо, вы также можете просто предоставить соответствующую функцию расширения. Но назвать это уже может быть не так просто ... Вот пример:
inline fun <K, V, W, T> Map<K, V>.intersectByKeyAndMap(otherMap : Map<K, W>, transformationFunction : (V, W) -> T) = flatMap { oldEntry ->
otherMap.filterKeys { it == oldEntry.key }
.map { transformationFunction(oldEntry.value, it.value) }
}
И теперь вы можете вызвать эту функцию в любом месте, где хотите пересечь карты по их ключам, и сразу же отобразить какое-либо другое значение, например следующее:
val actual = oldDogsMap.intersectByKeyAndMap(newDogsMap) { old, new -> new to old.second }
Обратите внимание, что я еще не большой поклонник нейминга. Но вы поймете суть ;-) Все вызывающие функции имеют приятный / короткий интерфейс, и им не нужно понимать, как это на самом деле реализовано. Тем не менее, сопровождающий функции должен, конечно, протестировать ее соответствующим образом.
Может также что-то вроде следующего помогает? Теперь мы вводим промежуточный объект, чтобы улучшить именование ... Все еще не уверен, но, возможно, это кому-то поможет:
class IntersectedMapIntermediate<K, V, W>(val map1 : Map<K, V>, val map2 : Map<K, W>) {
inline fun <reified T> mappingValuesTo(transformation: (V, W) -> T) = map1.flatMap { oldEntry ->
map2.filterKeys { it == oldEntry.key }
.map { transformation(oldEntry.value, it.value) }
}
}
fun <K, V, W> Map<K, V>.intersectByKey(otherMap : Map<K, W>) = IntersectedMapIntermediate(this, otherMap)
Если вы пойдете по этому пути, вам лучше позаботиться о том, что промежуточный объект действительно должен делать, например теперь я могу взять map1 или map2 из этого промежуточного звена, что может не подходить, если я посмотрю на его название ... так что у нас есть следующая строительная площадка ;-)
Большое спасибо за ваш ответ! Мне это нравится, потому что он решает мою проблему, заключающуюся в том, что я не обрабатываю значение NULL явно - и это работает! Тем не менее, мне интересно, может ли он быть лучше с точки зрения читаемости - я почти уверен, что в будущем я наткнусь на этот код и задаюсь вопросом, что он делает. Как указал Марко, я хочу сделать пересечение, хотя Kotlin предоставляет только пересечение итераций. Что-то вроде oldIds.intersect(newIds) работает, но мне понадобится oldDogsMap.intersect(newDogsMap), который возвращает карту, содержащую пару обоих значений. Есть ли функция, которая это делает?
Что ж, есть что-то подобное, но с вашим текущим data class или имеющейся у вас настройкой это сделать не так просто. Я обновлю свой ответ, чтобы показать вам другой подход, который может сработать для вас, например, добавив cute в качестве свойства в Dog.
Это просто мой неудачно сделанный пример - класс Dog не может (и не должен) содержать свою метаинформацию, потому что она используется только в одном месте кода.
ну ... если бы это было возможно, вы могли бы выполнить весь пример, используя MutableMap и его merge-метод. Если вы этого не сделаете, тогда ... ну ... вы все равно можете убедиться, что он выглядит как две равные карты (например, обе содержат Pair, хотя Pair второго содержит null в качестве значений ...) ... но тогда так много не выиграешь ;-)
Если вы не хотите улучшать data class или использовать MutableMap, тогда я бы предпочел скрыть всю эту функциональность в первую очередь (функция расширения?), И поэтому вызывающие абоненты увидят хороший код, тогда как сопровождающий должен обеспечить функциональность с помощью соответствующих тестов. ... Беспроигрышный вариант для всех ;-) Я обновил ответ по этому поводу ....
В итоге я использовал ваше решение с функцией расширения - в конце концов я назвал его intersectWith. Как указал Марко, реализация может использовать изменяемую карту для повышения производительности, но пока я выбрал решение flatMap. Код здесь для тех, кому в будущем понадобится функция intersectWith: gist.github.com/mreichelt/a6e4b85c204962e4f1248324ec3bb418
Я тоже так думал вначале ... :-) Но при вызове простого intersect я ожидал, что две карты будут пересекаться, но что это вообще будет означать с картами? Поэтому я подумал, что добавление byKey, вероятно, имеет смысл. С другой стороны, я подумал, что, вероятно, лучше буду ожидать, что в качестве возвращаемого значения будет Map, но сейчас мы возвращаем List. Так что поигравшись, я остановился на intersectByKeyAndMap; еще раз: я не так уверен в этом. Но, честно говоря, intersectWith кажется мне немного вводящим в заблуждение ... (как насчет: map1.intersectByKey(map2).mappingTo(transform)?) ... но это совсем другая проблема ... ;-)
Вы правы - и я ожидал, что метод intersectByKey или intersectWith также вернет Map, а не List. Скоро обновлю код ™ :-)
хм ... однако crossctBykey не может вернуть карту напрямую, так как вы не знаете, как ее преобразовать ... Создание карты, в которой она вам не нужна, тоже не так полезно ... Это, вероятно, причина, почему merge работает с отдельными ключами / значениями и принимает функцию преобразования :-) Я обновил ответ, чтобы показать одно из многих возможных решений для промежуточных объектов ;-) Чем больше я думаю об этом ... мы делаем то, чего не должны не делайте ... настройте свой класс данных соответствующим образом, и все будет работать лучше ;-)
Вы можете попробовать что-то вроде:
val actual = dogsAreCute.map {cuteDog -> cuteDog to newDogs.firstOrNull { it.id == cuteDog.first.id } }
.filter { it.second != null }
.map { it.second to it.first.second }
Это сначала связывает милых собак с новой собакой или нулевым, затем, если есть новая собака, сопоставляет пару: новая собака и информация о привлекательности из исходной карты.
Обновлять: Роланд прав, это возвращает тип List<Pair<Dog?, Boolean>>, поэтому вот предлагаемое исправление для типа для этого подхода:
val actual = dogsAreCute.mapNotNull { cuteDog ->
newDogs.firstOrNull { it.id == cuteDog.first.id }?.let { cuteDog to it } }
.map { it.second to it.first.second }
Скорее всего, его подход в другом ответе с использованием flatMap является более сложным решением.
Но это возвращает List<Pair<Dog?, Boolean>> вместо List<Pair<Dog, Boolean>> ... возможно, не то, что хотел OP ...
Вместо этого вам может потребоваться что-то вроде dogsAreCute.mapNotNull {cuteDog -> newDogs.firstOrNull { it.id == cuteDog.first.id }?.let { cuteDog to it } }.map { it.second to it.first.second } ... Однако затем вы просто заменили небезопасный оператор !! некоторыми безопасными операторами ?. / firstNotNull / mapNotNull, которые могут быть не такими удобочитаемыми, как сам !! ...
@ Роланд: Ах да, ты прав. Я настолько избалован умным кастом, что даже не проверил реальный тип возврата :)
Для упрощения предположим, что у вас есть следующее:
val data = mutableMapOf("a" to 1, "b" to 2)
val updateBatch = mapOf("a" to 10, "c" to 3)
Лучшим вариантом с точки зрения памяти и производительности является обновление записей непосредственно в изменяемой карте:
data.entries.forEach { entry ->
updateBatch[entry.key]?.also { entry.setValue(it) }
}
Если у вас есть причина придерживаться неизменяемых карт, вам придется выделить временные объекты и выполнить больше работы в целом. Сделать это можно так:
val data = mapOf("a" to 1, "b" to 2)
val updateBatch = mapOf("a" to 10, "c" to 3)
val updates = updateBatch
.filterKeys(data::containsKey)
.mapValues { computeNewVal(data[it.key]) }
val newData = data + updates
Это выглядит элегантно, но удаляет данные первой карты, которые мне нужно сохранить. Я не хочу полностью заменять данные, но хочу сохранить некоторую метаинформацию о них.
Вместо entry.setValue(it) вы можете писать все, что хотите, чтобы объединить новые данные со старыми.
Но он вводит изменяемость исходной карты, а также класса данных.
Это вам решать. Изменяемость более эффективна, но если у вас есть причина сохранить все неизменным и скопировать данные, тогда вам придется вычислить пересечение, а затем вычислить на его основе новую карту.
Вы говорите «объединение», но ваш код делает пересечение.
expectedсодержит только обновленные записи (которые уже существуют и являются частью обновленного пакета).