58

有人能解释一下copyKotlin 数据类的方法是如何工作的吗?对于某些成员来说,似乎实际上并未创建(深层)副本,并且引用仍然是原始的。

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)

输出:
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也是一个数据类,我希望在执行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])

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

4

8 回答 8

58

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

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)
于 2017-11-18T00:27:57.467 回答
10

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

这是一个带有示例 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
} 

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

于 2019-01-12T08:36:21.360 回答
10

正如@Ekeko 所说,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)
于 2017-11-18T09:57:41.240 回答
8

当心那些只是将列表引用从旧对象复制到新对象的答案。我发现的唯一快速的深度复制方法是序列化/反序列化对象,即将对象转换为 JSON,然后将它们转换回 POJO。如果您使用的是 GSON,下面是一段代码:

class Foo {
    fun deepCopy() : Foo {
        return Gson().fromJson(Gson().toJson(this), this.javaClass)
    }
}
于 2019-11-14T06:23:19.750 回答
1

基于先前的答案,一个简单但有点不雅的解决方案是使用该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))
    }

这不会像手写复制功能那么快,但如果对象发生更改,它不需要更新,因此更健壮。

于 2019-06-01T22:17:56.517 回答
1

我面临同样的问题。因为在 kotlin 中,如果成员是this中另一个对象的列表,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 并在那里修改 Flight 对象的内容

bindingView.imageViewReset.setOnClickListener {
        val temporaryList = ArrayList(makeProposalFragmentArgs.selectedFlightList.asList())
        val flightList = toFlightList(flightListToString(temporaryList))!!
        **adapter**.resetListToOriginal(flightList)
    }
于 2021-02-04T12:50:00.620 回答
0

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

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
}
于 2021-01-08T15:49:02.003 回答
0

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

private val objectMapper = ObjectMapper()
.registerModule(KotlinModule())
.registerModule(JavaTimeModule())
    
fun <T> Any.copyDeep(): T {
        return objectMapper.readValue(objectMapper.writeValueAsString(this), this.javaClass) as T
    }
于 2021-10-14T15:51:36.253 回答