Kotlin数据类复制方法不能深度复制所有成员

时间:2017-11-17 21:23:27

标签: kotlin copy deep-copy data-class

有人可以解释Kotlin数据类的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)
  

输出:
  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])

但我是否遗漏了某些内容,或者是否有更好的方法来执行此操作而无需指定这些成员需要强制执行深层复制?

7 个答案:

答案 0 :(得分:29)

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)

答案 1 :(得分:2)

正如@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)

答案 2 :(得分:2)

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

以下是示例Kotlin代码https://rosettacode.org/wiki/Deepcopy#Kotlin

的说明

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

答案 3 :(得分:0)

在先前的答案的基础上,一个简单的解决方案是使用kotlinx.serialization工具。按照文档将插件添加到build.gradle,然后制作对象的深层副本,并用@Serializable进行注释,并添加一个copy方法,该方法将对象转换为序列化的二进制形式,然后再次返回。新对象将不会引用原始对象。

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))
    }

这不会像手写复制功能那样快,但是如果更改了对象,则不需要更新,因此更加健壮。

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

也许你可以在这里以某种方式使用 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
}

答案 6 :(得分:0)

我面临同样的问题。因为在 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)
    }