Метод копирования класса данных Kotlin не выполняет глубокое копирование всех членов

Может ли кто-нибудь объяснить, как работает метод copy для классов данных Kotlin? Кажется, что для некоторых участников (глубокая) копия на самом деле не создана, а ссылки по-прежнему принадлежат оригиналу.

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

Но я что-то упустил или есть лучший способ сделать это, не указывая, что этим членам нужно заставить глубокую копию?

Ответ 1

Метод 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)

Ответ 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)

Ответ 3

Существует способ сделать глубокую копию объекта в 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 является более эффективным.

Ответ 4

Основываясь на предыдущем ответе, простое, хотя и несколько не элегантное решение заключается в использовании средства 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))
    }

Это не будет так быстро, как функция рукописного копирования, но не требует обновления, если объект изменен, поэтому более надежен.