Avatar ·

Передача по значению или по ссылке в Kotlin: почему я не могу изменить число в функции?

Всем привет! Я только начал изучать Kotlin после Java и запутался в механизме передачи аргументов в функции. Прочитал несколько статей, но до конца не понял. Вот простой пример. Я пишу функцию, которая должна увеличивать переданное число на 10: ```kotlin fun increaseValue(x: Int) { x = x + 10 // Ошибка компиляции: Val cannot be reassigned } ``` Компилятор ругается, что x — это val. Я понимаю, что параметры в Kotlin — это неизменяемые ссылки (val). Но тогда как работает передача для изменяемых объектов? Вот другой пример: ```kotlin data class Person(var name: String) fun changeName(person: Person) { person.name = "New Name" // Это работает } fun main() { val p = Person("Old") changeName(p) println(p.name) // Выведет "New Name" } ``` Здесь я могу изменить поле объекта. Значит ли это, что для примитивов передача по значению, а для объектов — по ссылке? Но если я попробую заменить сам объект внутри функции: ```kotlin fun replacePerson(person: Person) { person = Person("Another") // Ошибка: Val cannot be reassigned } ``` Снова ошибка. Почему в одном случае я могу изменить объект, а в другом нет? И что происходит с примитивами? Объясните, пожалуйста, как на самом деле работает передача аргументов в Kotlin. Хочется понять фундаментальный принцип, а не просто запомнить частные случаи. <<>>

Краткий ответ: в Kotlin всегда передача по значению (pass-by-value)

Но это значение — всегда копия ссылки (или копия самого примитива). Никакой "передачи по ссылке" в классическом понимании (как в C++ с &) в Kotlin нет. Давайте разберемся на примерах.

Что такое "значение" для разных типов?

  • Примитивные типы (Int, Double, Boolean и т.д.): хранят само значение. При передаче в функцию копируется число.
  • Ссылочные типы (классы, строки, массивы): переменная хранит ссылку (адрес в памяти) на объект. При передаче копируется эта ссылка, а не сам объект.

Разбор вашего первого примера (Int)

fun increaseValue(x: Int) {
    x = x + 10 // Ошибка! x — это val (копия числа)
}

Параметр x — это копия исходного числа. Даже если бы компилятор разрешил его менять, это не повлияло бы на оригинал. В Kotlin параметры всегда val, чтобы избежать путаницы: вы не можете случайно изменить копию, думая, что меняете оригинал.

Разбор второго примера (Person)

fun changeName(person: Person) {
    person.name = "New Name" // Работает
}

Здесь person — это копия ссылки на объект Person. Оригинальная переменная p в main() и параметр person в функции — это две разные переменные, но они ссылаются на один и тот же объект в памяти. Поэтому через копию ссылки вы можете изменять поля этого объекта.

Разбор третьего примера (замена объекта)

fun replacePerson(person: Person) {
    person = Person("Another") // Ошибка! person — это val
}

Даже если бы Kotlin разрешил переназначить person (сделав его var), это не повлияло бы на оригинальную переменную p в main(). Вы бы просто изменили локальную копию ссылки, заставив ее указывать на новый объект. Оригинал p по-прежнему указывал бы на старый объект.

Визуализация в памяти

Представьте, что объекты — это дома, а переменные — листочки с адресами домов.

  • val p = Person("Old") — вы записали адрес дома на листочек p.
  • changeName(p) — вы копируете адрес на новый листочек person. Теперь у вас два листочка с одинаковым адресом.
  • person.name = "New Name" — вы заходите в дом по адресу и перекрашиваете дверь. Второй листочек (p) показывает на тот же дом, поэтому изменение видно.
  • person = Person("Another") — вы пытаетесь стереть адрес на листочке person и написать новый. Листочек p при этом не меняется — он по-прежнему показывает на старый дом.

Как всё-таки изменить примитив внутри функции?

Есть несколько способов:

  1. Вернуть новое значение (самый идиоматичный способ в Kotlin):
fun increaseValue(x: Int): Int {
    return x + 10
}

fun main() {
    var a = 5
    a = increaseValue(a)
    println(a) // 15
}
  1. Использовать класс-обёртку (например, IntWrapper):
class IntWrapper(var value: Int)

fun increaseValue(wrapper: IntWrapper) {
    wrapper.value += 10
}

fun main() {
    val w = IntWrapper(5)
    increaseValue(w)
    println(w.value) // 15
}
  1. Использовать массив (редко, но работает):
fun increaseValue(arr: IntArray) {
    arr[0] += 10
}

fun main() {
    val arr = intArrayOf(5)
    increaseValue(arr)
    println(arr[0]) // 15
}

Итог

Запомните золотое правило: Kotlin — всегда pass-by-value. Для примитивов копируется само значение, для объектов — копируется ссылка. Вы никогда не можете изменить то, на что указывает оригинальная переменная (т.е. заставить её ссылаться на другой объект), но можете изменять содержимое объекта, если он мутабельный.

<<>>

Дополнение: что насчёт Java?

Если вы пришли из Java, то в Kotlin ровно такая же модель. В Java тоже всегда передача по значению (pass-by-value). Просто в Java принято говорить: "примитивы передаются по значению, объекты — по значению ссылки". Kotlin делает это правило более строгим, запрещая переназначение параметров (val вместо var).

Практический совет: используйте неизменяемые объекты

Чтобы избежать путаницы, старайтесь использовать val для свойств классов и неизменяемые коллекции. Например:

data class Person(val name: String) // name теперь val

fun main() {
    val p = Person("Old")
    // p.name = "New" // Ошибка компиляции — безопасно!
    
    // Для изменения создаём новый объект
    val p2 = p.copy(name = "New")
    println(p2.name) // New
}

Такой подход делает код более предсказуемым и безопасным в многопоточной среде. Функции, которые не изменяют переданные объекты (pure functions), легче тестировать и отлаживать.