Kotlin数据类的复制方法未深度复制所有成员。

78

有人能解释一下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.copy()` 时,`foo.bar` 也会被复制。
要深复制所有成员,可以这样做:
val fooCopy = foo.copy(bar = foo.bar.copy(), list = foo.list.toMutableList())

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

但是我是否漏掉了什么,或者有没有更好的方法可以在不需要指定这些成员需要强制进行深层复制的情况下完成此操作?

12个回答

73

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)

12
复制列表时要小心,因为这样复制的是同一内存位置,所以改变其中一个列表将会影响到另一个。如果需要进行真正的深层次复制,还需要复制列表项。请参阅 https://dev59.com/plUK5IYBdhLWcg3wqBbE。 - Big McLargeHuge
3
这个答案不应被接受。像这样复制仍然只是复制列表的引用。请查看我的答案以获取详细信息。 - Muhammad Muzammil
6
这个问题不是关于如何实现正确的深拷贝,而是“能否有人解释一下Kotlin数据类中的copy方法是如何工作的?” - Ekeko
1
@Ekeko 我不同意。这是关于深拷贝的正确实现。请看这个:“但我是否遗漏了什么,或者有没有更好的方法可以在不需要指定这些成员需要强制进行深拷贝的情况下完成?” - Muhammad Muzammil
@Ekeko 我写了一个单元测试来验证数据类上的 .copy() 方法的行为,并且复制的对象是一个 深拷贝。https://pastes.io/bdmzouljgz 你能解释一下为什么你坚持认为它是一个浅拷贝吗? - undefined

12

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

通过使用kotlin扩展函数,最好通过扩展类来添加新方法,而不是覆盖现有的方法 - 让我们保留默认行为不变,而不会通过在同一名称下混合浅拷贝深拷贝来增加更多复杂性。 - undefined

12
在 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
} 

注意:这个解决方案也适用于Android,但需要使用Parcelable接口而不是Serializable。Parcelable更加高效。

2
非常好。我认为,如果有这样的扩展,它可能会更有用:fun <T:Serializable?>T.deepCopy(): T? { if (this == null) return null val baos = ByteArrayOutputStream() val oos = ObjectOutputStream(baos) oos.writeObject(this) oos.close() val bais = ByteArrayInputStream(baos.toByteArray()) val ois = ObjectInputStream(bais) @Suppress("unchecked_cast") return ois.readObject() as T } - Burak Dizlek
1
这个实现肯定有问题,否则Java/Kotlin团队为什么不会添加相同的功能呢?或者我在这方面缺少一些重要信息。 - Farid
2
@Farid,这不是创建深拷贝的最佳性能方式。 - Sebas LG

12

注意那些仅仅将旧对象的列表引用复制到新对象中的答案。一种快捷但不是很高效的深度复制方法是将对象序列化/反序列化,即将对象转换为JSON,然后将其转换回POJO。 如果您使用的是GSON,请参考下面的代码:

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

1
要么序列化/反序列化对象,要么将其转换为JSON格式。因此,您暗示将其转换为/从JSON格式转换是与序列化/反序列化本身不同的事情。 - ratijas
30
这太糟糕了,请不要这样做。这比例如apache commons的简单深度复制函数慢1000倍,并且使用的内存量也多1000倍。 - Agoston Horvath
@MuhammadMuzammil 你是否运行了一个性能测试循环,来深拷贝一定大小(比如1KB)的对象,使用JSON序列化/反序列化,与ByteArrayOutputStream序列化/反序列化以及痛苦的(写起来麻烦的)Kotlin copy()手动覆盖? - Paulo Merson
@PauloMerson 没有,我确定这是一项相当繁重的工作。虽然我提到这是一种“快速”完成任务的方法,但可能不是最好的方法。 - Muhammad Muzammil
2
@MuhammadMuzammil 当没有限定条件时,这是一个模糊的词。所提出的解决方案很快(即易于实现),但可能不会很快地运行。 - Paulo Merson
显示剩余9条评论

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

这个函数不像手写的复制函数那样快,但如果对象被更改,它不需要更新,因此更加健壮。

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

与评论中描述的代码相同的问题,请参考此链接https://stackoverflow.com/a/58850583/3649629。 - undefined
@Gleichmut 你试过我的解决方案了吗? - undefined
Shihab,我并没有启动这段代码。我是通过研究Kotlin中的shallowdeep复制的细微差别来到这个讨论串的。我在上面链接的回答帖子的评论中提到了我的观点,但我会在这里重复一遍:这段代码在我迄今为止所在的任何团队的代码审查中都不会通过。这是一种不良实践,除非你真的需要快速而粗糙地交付某些东西,否则不应该使用。 - undefined

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
}

0

你需要的是深拷贝。有许多工具可用于此。

  1. MapStruct: https://mapstruct.org/

Mapstruct在编译时生成代码。通常,它是用于自动生成Java对象之间的映射器,但它也具有“克隆”功能,可以创建对象的深拷贝。由于这是手动编写的生成代码,因此这是实现此目的的最快方法。

还有许多其他工具(如kryo、dozer等),您实际上只需搜索即可,例如在此处:https://programmer.group/performance-comparison-between-shallow-and-deep-copies.html

请避免使用基于序列化的“克隆”:apache commons' SerializationUtils、jackson、gson等。它们具有巨大的开销,因为它首先创建中间状态。它们比实际复制慢10-100倍。


0

如果您有正文参数,它们也不会被复制。许多程序员都会像这样保留他们的方法:

data class Data(
    val id: Int,
    val images: List<Image> = emptyList()
)  {
    // Body parameters
    var onImageClick: () -> Unit = { }
    var onLikeClick: () -> Unit = { }
}

当您复制该对象时,将获得一个具有空(默认)正文参数(onImageClick和onLikeClick)的新对象。为避免这种情况,只需添加一个新方法。请注意,我使用apply

data class Data(
    val id: Int,
    val images: List<Image> = emptyList()
)  {
    var onImageClick: () -> Unit = { }
    var onLikeClick: () -> Unit = { }

    fun deepCopy(
        id: Int = this.id,
        images: List<Image> = this.images,
        onImageClickAction: () -> Unit = this.onImageClick,
        onLikeClickAction: () -> Unit = this.onLikeClick
    ) = Data(id = id,
            // Use deep copy of the list from above answers instead
            images = images).apply {
        onImageClick = onImageClickAction
        onLikeClick = onLikeClickAction
    }
}

0
这里有一种方法,使用Kotlin反射和扩展函数来调用成员上的copy()。语法不如copy()那么简洁,但很接近。
如果我们有这些数据类:
data class Foo(
    val name: String,
)
data class Bar(
    val id: Long,
    val foo: Foo,
)

然后代码看起来像这样:

val template = Bar(...)
val copy = template.copy(id = 1)
    .copyFoo(Foo::name to "new name")

扩展功能:
fun Bar.copyFoo(
    vararg overrides: Pair<KProperty1<Foo, *>, Any>
) {
    val oldFoo = this.foo
    val newFoo = copyMember<T>(oldFoo, *overrides)
    return this.copy(
       foo = newFoo
    )
}

或者紧凑:

fun Bar.copyFoo(
    vararg overrides: Pair<KProperty1<Foo, *>, Any>
) {
    return this.copy(
       foo = copyMember<T>(this.foo, *overrides)
    )
}

通用的copyMember函数如下所示。
/**
 * Dynamically copy a non-primitive member.
 *
 * Duplicate overrides are merged by keeping the last one.
 */
inline fun <reified T> copyMember(
    member: T,
    // The type T makes sure that all properties are members of the same type
    vararg overrides: Pair<KProperty1<T, *>, Any>
): T {
    val lookup = overrides.associateBy(
        { it.first.name },
        { it.second }
    )
    // Find the copy function of the member type
    val copyFn = T::class.memberFunctions.single { it.name == "copy" }
    // The copy function has an additional hidden parameter which contains "this" during the copy operation
    val instanceParam = copyFn.instanceParameter!!
    // These are the usual parameters for copy()
    val overrideParameters = copyFn.parameters
        .filter {
            lookup.containsKey(it.name)
        }
        .map {
            it to lookup[it.name]
        }
    val parameters = (listOf(instanceParam to member) + overrideParameters)
        .toMap()

    // Call copy with the instance and the overrides
    return copyFn.callBy(parameters) as T
}

该代码在很大程度上依赖于类型推断和copy()中的内部检查,以确保输出有效。

它比序列化类更快,但需要更多的代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接