Kotlin 数据 class 复制方法未深度复制所有成员

Kotlin data class copy method not deep copying all members

有人可以解释 Kotlin 数据 classes 的 copy 方法究竟是如何工作的吗?对于某些成员来说,似乎并没有实际创建(深)副本,并且引用仍然是原始的。

fun test() {
    val bar = Bar(0)
    val foo = Foo(5, bar, mutableListOf(1, 2, 3))
    println("foo    : $foo")

    val barCopy = bar.copy()
    val fooCopy = foo.copy()
    foo.a = 10
    bar.x = 2
    foo.list.add(4)

    println("foo    : $foo")
    println("fooCopy: $fooCopy")
    println("barCopy: $barCopy")
}

data class Foo(var a: Int,
               val bar: Bar,
               val list: MutableList<Int> = mutableListOf())

data class Bar(var x: Int = 0)

Output:
foo : Foo(a=5, bar=Bar(x=0), list=[1, 2, 3])
foo : Foo(a=10, bar=Bar(x=2), list=[1, 2, 3, 4])
fooCopy: Foo(a=5, bar=Bar(x=2), list=[1, 2, 3, 4])
barCopy: Bar(x=0)

为什么是 barCopy.x=0(预期),但 fooCopy.bar.x=2(我认为它会是 0)。由于 Bar 也是一个数据 class,我希望 foo.bar 在执行 foo.copy() 时也是一个副本。

要深度复制所有成员,我可以这样做:

val fooCopy = foo.copy(bar = foo.bar.copy(), list = foo.list.toMutableList())

fooCopy: Foo(a=5, bar=Bar(x=0), list=[1, 2, 3])

但是我是不是遗漏了什么,或者是否有更好的方法可以做到这一点而无需指定这些成员需要强制进行深层复制?

Kotlin 的copy 方法根本不应该是深拷贝。如参考文档 (https://kotlinlang.org/docs/reference/data-classes.html) 中所述,对于 class 例如:

data class User(val name: String = "", val age: Int = 0)

copy 实施将是:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

如您所见,这是一个浅拷贝。 copy 在您的特定情况下的实现将是:

fun copy(a: Int = this.a, bar: Bar = this.bar, list: MutableList<Int> = this.list) = Foo(a, bar, list)

fun copy(x: Int = this.x) = Bar(x)

正如@Ekeko 所说,为数据 class 实现的默认 copy() 函数是一个浅拷贝,如下所示:

fun copy(a: Int = this.a, bar: Bar = this.bar, list: MutableList<Int> = this.list)

要执行深层复制,您必须覆盖 copy() 函数。

fun copy(a: Int = this.a, bar: Bar = this.bar.copy(), list: MutableList<Int> = this.list.toList()) = Foo(a, bar, list)

有一种方法可以在 Kotlin(和 Java)中对对象进行深度复制:将其序列化到内存中,然后将其反序列化回到一个新的目的。这仅在对象中包含的所有数据都是原始数据或实现可序列化接口时才有效

这里有一个示例 Kotlin 代码的解释 https://rosettacode.org/wiki/Deepcopy#Kotlin

import java.io.Serializable
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream
import java.io.ObjectOutputStream
import java.io.ObjectInputStream

fun <T : Serializable> deepCopy(obj: T?): T? {
    if (obj == null) return null
    val baos = ByteArrayOutputStream()
    val oos  = ObjectOutputStream(baos)
    oos.writeObject(obj)
    oos.close()
    val bais = ByteArrayInputStream(baos.toByteArray())
    val ois  = ObjectInputStream(bais)
    @Suppress("unchecked_cast")
    return ois.readObject() as T
} 

注意:此解决方案也应该适用于 Android 使用 Parcelable 接口而不是 Serializable。 Parcelable 效率更高。

基于之前的答案,一个简单但有些不雅的解决方案是使用 kotlinx.serialization 工具。根据文档将插件添加到 build.gradle,然后制作一个对象的深拷贝,用 @Serializable 注释它并添加一个将对象转换为序列化二进制形式的复制方法,然后再返回.新对象将不会引用原始对象中的任何对象。

import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor

@Serializable
data class DataClass(val yourData: Whatever, val yourList: List<Stuff>) {

    var moreStuff: Map<String, String> = mapOf()

    fun copy(): DataClass {
        return Cbor.load(serializer(), Cbor.dump(serializer(), this))
    }

这不会像手写复制函数那样快,但如果对象发生变化,它不需要更新,因此更健壮。

当心那些只是将列表引用从旧对象复制到新对象的答案。深度复制的一种快速方法(虽然不是很有效)是 serialize/deserialize 对象,即将对象转换为 JSON 然后将它们转换回 POJO。 如果您使用的是 GSON,这里有一段快速代码:

class Foo {
    fun deepCopy() : Foo {
        return Gson().fromJson(Gson().toJson(this), this.javaClass)
    }
}

也许你可以在这里以某种方式使用 kotlin reflection,这个例子不是递归的,但应该给出思路:

fun DataType.deepCopy() : DataType {
    val copy = DataType()

    for (m in this::class.members) {
        if (m is KProperty && m is KMutableProperty) {
            m.setter.call(copy, if (m.returnType::class.isData) {
                (m.getter.call(this) to m.returnType).copy()
            } else m.setter.call(copy, m.getter.call(this)))
        }
    }

    return copy
}

我遇到了同样的问题。因为在 kotlin 中,如果一个成员是 另一个对象的列表ArrayList.map {it.copy} 不会特别复制一个对象的所有项目。

对于我在网上找到的对象的所有项目的深度复制,唯一的解决方案是序列化反序列化发送或分配给新变量时的对象。代码如下。

@Parcelize
data class Flights(

// data with different types including the list 
    
) : Parcelable

在我收到航班列表之前,我们可以使用JSON反序列化对象并同时序列化对象!!!

首先,我们创建两个扩展函数。

// deserialize method
fun flightListToString(list: ArrayList<Flights>): String {
    val type = object : TypeToken<ArrayList<Flights>>() {}.type
    return Gson().toJson(list, type)
}

// serialize method
fun toFlightList(string: String): List<Flights>? {
    val itemType = object : TypeToken<ArrayList<Flights>>() {}.type
    return Gson().fromJson<ArrayList<Flights>>(string, itemType)
}

我们可以像下面这样使用它。

   // here I assign list from Navigation args

    private lateinit var originalFlightList: List<Flights>
    ...
    val temporaryList = ArrayList(makeProposalFragmentArgs.selectedFlightList.asList())    
    originalFlightList = toFlightList(flightListToString(temporaryList))!! 

稍后,我将此列表发送到 Recycler Adapter,然后 Flights 对象的内容将被修改。

bindingView.imageViewReset.setOnClickListener {
        val temporaryList = ArrayList(makeProposalFragmentArgs.selectedFlightList.asList())
        val flightList = toFlightList(flightListToString(temporaryList))!!
        **adapter**.resetListToOriginal(flightList)
    }

如果您使用 Jackson 并且不关心性能,那么这个简单的扩展函数将为您提供此功能。

private val objectMapper = ObjectMapper()
.registerModule(KotlinModule())
.registerModule(JavaTimeModule())
    
fun <T> Any.copyDeep(): T {
        return objectMapper.readValue(objectMapper.writeValueAsString(this), this.javaClass) as T
    }

使用这个函数:

private val gson = Gson()

fun <T> deepCopy(item: T?, clazz: Class<T>): T {
        val str = gson.toJson(item)
        return gson.fromJson(str, clazz)
    }

你要的是深拷贝。有许多工具可用于执行此操作。

  1. 地图结构:https://mapstruct.org/

Mapstruct 在编译时生成代码。通常,它是 java 对象之间的 auto-generate 映射器,但它也有一个 'clone' 功能来创建对象的深拷贝。由于这是您手动编写的生成代码,因此这是实现此目的的最快方法。

还有很多(kryo、dozer 等...),实际上您可以 google,例如这里:https://programmer.group/performance-comparison-between-shallow-and-deep-copies.html

DO AVOID serialization-based 'clone':apache commons 的 SerializationUtils、jackson、gson 等...它们有巨大的开销,因为它首先创建一个中间状态。它们比实际复制慢大约 10-100 倍。