【问题标题】:Get intersection of two maps with different values in Kotlin在 Kotlin 中获取具有不同值的两个地图的交集
【发布时间】:2023-04-09 21:21:01
【问题描述】:

我有两个列表:一个包含应保留 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 变量的更好方法是什么?我特别不喜欢我首先过滤 both 列表中包含的键,然后我必须显式使用 newDogsMap[it.key]!! 来获取 null 安全值。

我该如何改进它?

编辑:重新定义问题

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

【问题讨论】:

  • 你说“联合”,但你的代码做了一个交集。 expected 仅包含更新的条目(它们都已经存在并且是更新批次的一部分)。
  • 你说得对——我想做一个交叉路口。但我需要的是地图上的交叉点——据我所知 Kotlin 只支持列表上的交叉点。
  • 如果你的两个映射包含相同的值类型,你可以使用merge,只要你有一个MutableMap。如果您没有并且不想与null-safe-operators 合作,那么我不知道任何比显示的更简单的方法。相反,它只会变得更复杂,或者您编写更多代码,我认为省略另一个 null-safe-operator 不会使其更具可读性;-)

标签: unit-testing collections kotlin functional-programming


【解决方案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 安全操作(例如!!?.mapNotNullfirstOrNull() 等)。

另一种方法是将cute 作为属性添加到data class Dog,并为新狗使用MutableMap。这样您就可以使用您自己的合并功能适当地merge 值。但正如你在 cmets 中所说,你不想要 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)

如果你走这条路,你应该关心中间对象应该真正被允许做的事情,例如现在我可以将map1map2 从那个中间体中取出,如果我看一下它的名字,这可能不合适......所以我们有了下一个建筑工地;-)

【讨论】:

  • 非常感谢您的回答!我喜欢它,因为它解决了我不明确处理可空性的问题——而且它有效!我仍然想知道它是否在可读性方面会更好——我很确定未来的我会偶然发现这段代码并想知道它的作用。正如 Marko 指出的那样,我想做一个交集——尽管 Kotlin 只提供了一个可迭代对象的交集。像oldIds.intersect(newIds) 这样的东西可以工作,但我需要oldDogsMap.intersect(newDogsMap) 来返回一个包含两个值的映射。有这样的功能吗?
  • 嗯,有一些类似的东西,但是使用您当前的data class 或您拥有的设置并不那么容易。我将更新我的答案,向您展示另一种可能对您有用的方法,即将cute 作为属性添加到Dog
  • 这只是我做的不好的例子 - Dog 类不能(也不应该)包含它的元信息,因为它只是在代码中的某一点使用。
  • 好吧...如果可以的话,您可以使用MutableMap 及其merge 方法来完成整个示例。如果你不这样做,那么......好吧......你仍然可以确保它看起来是两个相等的映射(例如,即使第二个的 Pair 包含 null 作为值,两者都包含 Pair ...... ) ... 但那样你就赢不了那么多了 ;-)
  • 如果您不想增强data class,也不想使用MutableMap,那么我宁愿首先隐藏所有这些功能(扩展功能?),所以调用者会看到漂亮的代码,而维护者需要通过适当的测试来确保功能......对所有人都是双赢的;-)我在这方面更新了答案......
【解决方案2】:

为了简化事情,假设您有以下内容:

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),您可以编写任何您想将新数据合并到旧数据中的内容。
  • 但是它引入了原始地图以及数据类的可变性。
  • 这由您决定。可变性更有效,但如果您有理由保持一切不可变并复制数据,那么您必须计算交点,然后从中计算新地图。
【解决方案3】:

你可以试试这样的:

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 }

这首先将可爱的狗与新狗配对或 null,然后如果有新狗,则映射到这对:原始地图中的新狗和可爱信息。

更新:Roland 是对的,这会返回 List&lt;Pair&lt;Dog?, Boolean&gt;&gt; 的类型,所以这里是针对这种方法的类型的建议修复:

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&lt;Pair&lt;Dog?, Boolean&gt;&gt; 而不是 List&lt;Pair&lt;Dog, Boolean&gt;&gt;... 可能不是 OP 想要的...
  • 您可能需要 dogsAreCute.mapNotNull {cuteDog -&gt; newDogs.firstOrNull { it.id == cuteDog.first.id }?.let { cuteDog to it } }.map { it.second to it.first.second } 之类的东西...但是,您只需将 !! 不安全运算符替换为一些 ?./firstNotNull/mapNotNull 安全运算符,这可能不会与!! 本身一样可读...
  • @Roland:啊,你说得对。我被聪明的演员宠坏了,我什至没有检查实际的返回类型:)
【解决方案4】:

你可以试试这个:

val intersection = map1.mapNotNull { (map1Key, map1Value) ->
    map2[map1Key]
        ?.let { map2Value -> map1Value to map2Value }
        ?.let { pair -> map1Key to pair }
}.toMap()

【讨论】:

    猜你喜欢
    • 2021-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-27
    相关资源
    最近更新 更多