【问题标题】:Construct a List<Map> object out of multiple List<Any> objects in Kotlin从 Kotlin 中的多个 List<Any> 对象构造一个 List<Map> 对象
【发布时间】:2022-01-23 01:10:33
【问题描述】:

我在 kotlin 中有多个不同的数据类,它们以完全相同的长度保存在额外的列表中。现在我想将它们组合成一个结果列表,其中包含它们作为具有相同长度(1000 个条目)的映射。我尝试了一个 for 循环和一个预填充的映射列表,但这对我来说看起来有点乱,所以我问自己是否有更清洁的方法来做到这一点,尤其是使用 kotlin。

data class One(
    val a: Double,
    val b: Double,
    var c: Double
)
data class Two(
    val d: Double,
    val e: Double,
)

fun main() {
    val oneList: List<One> = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
    val twoList: List<Two> = listOf(Two(5.0, 2.0), Two(7.0, 1.0))
    val results: List<MutableMap<String,Double>> = (0..1).map{ mutableMapOf() }
    for(i in 0 .. 1){
        results[i]["a"] = oneList[i].a
        results[i]["b"] = oneList[i].b
        results[i]["c"] = oneList[i].c
        results[i]["d"] = twoList[i].d
        results[i]["e"] = twoList[i].e
    }
}

问题是,我有大约 10 个类,每个对象中有大约 2-3 个成员,因此代码大约有 30 多行......结果应该如下所示:

[
    {
        a: 1.0,
        b: 0.0,
        c: 2.0,
        d: 5.0,
        e: 2.0
    },
    {
        a: 3.0,
        b: 5.0,
        c: 10.0,
        d: 7.0,
        e: 1.0
    }
]

【问题讨论】:

  • 我尝试了不同的方法:按索引分组、减少或折叠、使用 kotlin 反射或将对象转换为地图并将它们添加在一起。最后一种方法似乎是最好的解决方案。

标签: list kotlin maps


【解决方案1】:
import kotlin.reflect.full.declaredMemberProperties

data class One(val a: Double, val b: Double, var c: Double)
data class Two(val d: Double, val e: Double)

val oneList: List<One> = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
val twoList: List<Two> = listOf(Two(5.0, 2.0), Two(7.0, 1.0))

fun compounded(vararg lists: List<Any>): List<Map<String, Double>> {
  return lists
    .mapIndexed { index, _ ->
      lists
        .flatMap {
          it[index]::class.declaredMemberProperties.map { prop ->
            mapOf(prop.name to prop.call(it[index]) as Double)
          }
        }
        .fold(mapOf()) { acc, map ->
          mutableMapOf<String, Double>().apply { putAll(acc + map) }
        }
    }
}

val result = compounded(oneList, twoList)

【讨论】:

  • 我也尝试过使用 kotlin 反射和声明成员属性,但我认为将它们转换为地图是更好的方法。我还尝试通过收集的地图列表的索引对其进行分组,如下所示:lists.map { list -&gt; list.withIndex().groupByTo(mutableMapOf(), {it.index}, {it.value}) }。但这并没有按预期工作......
【解决方案2】:

所以在一天结束时,您想要一个属性名称的String 表示,映射到它的值?我认为你有两个选择:

  • 使用反射获取所有成员名称,过滤您想要的类型(例如,仅Doubles),因此您的Strings 在运行时直接从类派生
  • 在某处定义一个String,作为每个属性的标签,并尽量将其与类紧密联系起来,以便于维护。因为它不是从属性本身派生的,所以您必须使属性名称和它的标签保持同步 - 两者之间没有内在联系可以自动化它

您已经在代码中执行了后者 - 在构建 results 映射时,您正在使用诸如 "a""b" 之类的硬编码任意标签,这就是您将标签与属性连接起来的地方 (例如results[i]["a"] = oneList[i].a)。所以这里有另一种方法可以解决这个问题!

Kotlin playground

interface Mappable {
    // allows us to declare classes as having the property map we're using
    abstract val propertyMap: Map<String, Double>
}

data class One(
    val a: Double,
    val b: Double,
    var c: Double
) : Mappable {
    // you have to do this association somewhere - may as well be in the class itself
    // you could also use reflection to build this map automatically
    override val propertyMap = mapOf("a" to a, "b" to b, "c" to c)
}
data class Two(
    val d: Double,
    val e: Double,
) : Mappable {
    override val propertyMap = mapOf("d" to d, "e" to e)
}

fun main() {
    val oneList = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
    val twoList = listOf(Two(5.0, 2.0), Two(7.0, 1.0))
    combine(oneList, twoList).forEach(::println)
}

fun combine(vararg lists: List<Mappable>): List<Map<String, Double>> {
    // lists could be different lengths, so we go until one of them runs out
    val length = lists.minOf { it.size }
    
    return (0 until length).map { index ->
        // create a combined map for each index
        mutableMapOf<String, Double>().apply {
            // visit each list at this index, grabbing the property map from each object
            // and adding its contents to the combined map we're building
            lists.map { it.elementAt(index).propertyMap }.forEach(this::putAll)
        }
    }
}

>> {a=1.0, b=0.0, c=2.0, d=5.0, e=2.0}
{a=3.0, b=5.0, c=10.0, d=7.0, e=1.0}

问题实际上是您在属性和明确定义的标签之间引入的人为连接 - 您必须确保维护它,如果不这样做,就会出现错误。我认为这是不可避免的,除非你使用反射。

此外,如果这些数据最初来自诸如 JSON 之类的任意数据(验证和标记属性的问题在链中较早),您可以查看 Kotlin 的 map delegate 并看看这是否更好适合这种方法

【讨论】:

  • 这是迄今为止最好的答案。您不能在将对象列表传递给函数之前将它们转换为映射列表吗?像这样:objectMapper.convertValue(oneList, object: TypeReference&lt;List&lt;Map&lt;String,Double&gt;&gt;&gt;() {}).
  • @MartinNöbel 那是杰克逊,对吧?我不太熟悉它,但是如果您检查 map delegate 链接,它会向您展示如何定义一个 Kotlin 类,其属性引用您传入的映射中的键。例如a 的值为theMap["a"](这是进行属性/标签关联的地方,它是自动的,但地图需要有效)。这使得从 JSON 之类的东西构建对象变得很容易,但是你在代码中失去了便利(必须传递一个映射,不检查包含哪些属性)。所以是的,如果你不需要实际的对象,你可以直接传递地图
  • @MartinNöbel 但如果您的意思是您拥有 Kotlin 数据类,并且您想使用 Jackson 将其转换为 Map 表示...就像我说的那样,我并不熟悉它,如果这是可行的(并且避免在每个班级中保留propertyMap),那么确定!就我个人而言,那时我可能会考虑反射,但更多的是为了清晰和效率 - 最好的办法是尝试一下,看看它运行得如何!
【解决方案3】:

您可以使用.zip 来链接这两个列表,然后使用.map 和一个简单的mapOf() 直接创建列表来创建地图。

val results = oneList.zip(twoList).map { (one, two) ->
    mapOf("a" to one.a, "b" to one.b, "c" to one.c, "d" to two.d, "e" to two.e)
}

至于实际上为很多课程这样做......不是100%肯定。您could 可能会使用反射,但这通常不是您在实际程序中使用的东西。另一种方法是在每个提供地图的类中创建一个函数,然后将这些地图添加到上面的.map 中。这样,代码看起来会更干净一些。大致如下:

data class One(
    val a: Double,
    val b: Double,
    var c: Double
){
    fun toMap(): Map<String, Double> {
        return mapOf("a" to a, "b" to b, "c" to c)
    }
}

//.....

val results = oneList.zip(twoList).map { (one, two) -> one.toMap() + two.toMap() }

但您很快就会注意到zip 不能用于超过 2 个列表。我建议实施这样的事情:Zip multiple lists SO。但这也行不通,因为你的类是不同的类型。我要做的是创建一个具有toMap() 乐趣的抽象类,然后您需要的所有类都可以继承它。它看起来像这样:

abstract class MotherClass(){
    abstract fun toMap(): Map<String, Double>
}

data class One(
    val a: Double,
    val b: Double,
    var c: Double
):MotherClass() {
    override fun toMap(): Map<String, Double> {
        return mapOf("a" to a, "b" to b, "c" to c)
    }
}

// ...

val results = zip(oneList, twoList /*, threeList, etc */).map { list -> list.map { it.toMap() } }

【讨论】:

    猜你喜欢
    • 2012-06-22
    • 2020-12-07
    • 2021-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-07
    • 1970-01-01
    相关资源
    最近更新 更多