代码之家  ›  专栏  ›  技术社区  ›  mreichelt

在Kotlin中求两个不同值地图的交集

  •  4
  • mreichelt  · 技术社区  · 7 年前

    我有两个列表:一个是旧数据,其中 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]
    

    但我真正想要的是地图上的交集:

    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 回复  |  直到 7 年前
        1
  •  3
  •   Roland    7 年前

    干得好:

    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 -)安全。

    这样你就省去了那些 无效的 -安全操作(例如。 !! , ?. , mapNotNull , firstOrNull() 等等)。

    另一种方法是 cute 作为 data class Dog 并使用 MutableMap 换成新来的狗。这样你就可以 merge 使用您自己的合并函数适当地设置值。但正如你在评论中所说,你不想 可变映射 ,那就不行了。

    如果您不喜欢这里发生的事情,而是想向任何人隐藏它,您也可以只提供适当的扩展函数。但命名可能已经不那么容易了。。。下面是一个例子:

    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 中间产物,如果我看看它的名字可能不合适。。。所以我们有了下一个建筑工地;-)

        2
  •  1
  •   Marko Topolnik    7 年前

    为了简化问题,假设您有以下几点:

    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
    
        3
  •  1
  •   DVarga    7 年前

    你可以试试这样的方法:

    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 是一个更复杂的解决方案。

    推荐文章