首页 文章

在Kotlin中获取具有不同值的两个映射的交集

提问于
浏览
2

我有两个列表:一个包含旧数据,其中应保留 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]!! 来获取空值安全值 .

我怎样才能改进它?

编辑:问题已重新定义

感谢Marko:我想做一个十字路口,而不是一个工会 . 在列表上做一个交集是很容易的:

val list1 = listOf(1, 2, 3)
val list2 = listOf(4, 3, 2)
list1.intersect(list2)
// [2, 3]

但我真正想要的是 Map 上的交叉点:

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

3 回答

  • 1

    干得好:

    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 -safe操作(例如 !!?.mapNotNullfirstOrNull() 等) .

    另一种方法是将 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) }
    }
    

    现在,您可以在任何想要通过键交叉 Map 的位置调用此函数,并立即映射到其他值,如下所示:

    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)
    

    如果你走这条路,你应该注意中间对象应该被允许做什么,例如现在我可以从那个中间体中取出 map1map2 ,如果我查看它的名字可能不合适......所以我们有下一个施工现场;-)

  • 1

    为简化起见,假设您有以下内容:

    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
    
  • 1

    你可以尝试类似的东西:

    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 }
    

    这首先将可爱的狗与一只新狗配对或无效,然后如果有一只新狗,则映射到该对:来自原始 Map 的新狗和可爱信息 .

    更新:Roland是对的,这将返回 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 的另一个答案中的方法是一个更复杂的解决方案 .

相关问题