Передача по значению или по ссылке в 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при этом не меняется — он по-прежнему показывает на старый дом.
Как всё-таки изменить примитив внутри функции?
Есть несколько способов:
- Вернуть новое значение (самый идиоматичный способ в Kotlin):
fun increaseValue(x: Int): Int {
return x + 10
}
fun main() {
var a = 5
a = increaseValue(a)
println(a) // 15
}
- Использовать класс-обёртку (например,
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
}
- Использовать массив (редко, но работает):
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), легче тестировать и отлаживать.
Ответов пока нет.
Войдите чтобы оставить ответ