← Назад к вопросам

Как работает метод copy в data class с полями в теле класса?

2.0 Middle🔥 112 комментариев
#Kotlin основы#Архитектура и паттерны

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Механизм copy в data-классах с полями в теле класса

Метод copy() в Kotlin — это автоматически генерируемая функция для data-классов, которая создает копию объекта с возможностью изменения части свойств. Однако его поведение с полями, объявленными в теле класса (не в первичном конструкторе), имеет важные нюансы.

Ключевые особенности работы copy:

1. Поля в теле класса НЕ включаются в параметры copy Метод copy генерируется только для свойств, объявленных в первичном конструкторе. Поля, определенные в теле класса, игнорируются.

Рассмотрим пример:

data class Person(val name: String, val age: Int) {
    var nickname: String = ""
    
    fun printInfo() {
        println("$name ($nickname), $age лет")
    }
}

fun main() {
    val person1 = Person("Алексей", 30).apply { nickname = "Алекс" }
    val person2 = person1.copy(age = 31)
    
    println(person1.nickname) // "Алекс"
    println(person2.nickname) // "" (значение сброшено!)
    person2.nickname = "Леха"
    
    println(person1 == person2) // false (разные name/age)
}

2. Значения полей тела класса сбрасываются к значениям по умолчанию При вызове copy() создается новый объект, и все свойства в теле класса инициализируются заново — либо значениями по умолчанию, либо значениями из init-блоков.

3. Структурное сравнение (equals/hashCode) также игнорирует эти поля

data class Product(val id: Int) {
    var price: Double = 0.0
}

fun main() {
    val p1 = Product(1).apply { price = 100.0 }
    val p2 = Product(1).apply { price = 200.0 }
    
    println(p1 == p2) // true (только id учитывается)
    println(p1.hashCode() == p2.hashCode()) // true
}

Почему такое поведение?

Концептуальное обоснование: Data-классы предназначены для моделирования неизменяемых данных (value objects). Поля в теле класса обычно представляют:

  • Вычисляемые/производные свойства
  • Внутреннее состояние
  • Временные данные

Включение их в copy нарушило бы принцип предсказуемости и логического равенства.

Практические решения и обходные пути:

1. Вынос поля в параметр конструктора (если оно часть данных):

data class Person(val name: String, val age: Int, val nickname: String = "")

2. Ручная реализация copy при необходимости:

data class Person(val name: String, val age: Int) {
    var nickname: String = ""
    
    fun copy(
        name: String = this.name,
        age: Int = this.age,
        nickname: String = this.nickname
    ): Person {
        return Person(name, age).apply { this.nickname = nickname }
    }
}

3. Использование делегирования:

class PersonData(val name: String, val age: Int)
data class Person(val data: PersonData) {
    var nickname: String = ""
    
    fun copyPerson(
        data: PersonData = this.data,
        nickname: String = this.nickname
    ) = Person(data).apply { this.nickname = nickname }
}

Важные выводы:

  • Метод copy работает исключительно с свойствами первичного конструктора
  • Поля в теле класса не копируются, а инициализируются заново
  • Для комплексного копирования необходим кастомный метод copy
  • При проектировании data-классов следует тщательно выбирать, что является частью данных (идёт в конструктор), а что — вспомогательным состоянием

Это поведение обеспечивает консистентность data-классов как структур данных, предотвращая неявное копирование вспомогательного состояния, которое может быть контекстно-зависимым.