Передача по значению и по ссылке: как работают аргументы функций
Основы4 минРедакция Innovialabs
Коротко
Большинство популярных языков передают копию значения, а для объектов это значение — ссылка: функция может изменить общий объект, но не может переназначить переменную вызывающего кода; настоящая передача по ссылке есть только там, где язык это явно позволяет, например ref в C# или указатели в Go.
Короткий ответ
Встречаются три модели:
- Передача по значению — функция получает копию. Её изменение не влияет на вызывающий код.
- Передача по ссылке — функция получает псевдоним переменной вызывающего кода. Присваивание параметру меняет саму эту переменную.
- Передача по соиспользованию (call by sharing, «ссылка передаётся по значению») — функция получает копию ссылки на тот же объект. Она может изменить объект, но переприсваивание параметра меняет только локальную копию.
Python, JavaScript и Java для объектов используют третью модель. Поэтому популярное утверждение «Java передаёт по ссылке» неверно.
По языкам
| Язык | По умолчанию | Настоящая передача по ссылке |
|---|---|---|
| Python | Соиспользование (всё — объекты) | Нет |
| JavaScript | Значение для примитивов, соиспользование для объектов | Нет |
| Java | Значение для примитивов, соиспользование для объектов | Нет |
| Go | Всегда значение; срезы и map содержат внутренние указатели | Указатели (*T) |
| C# | Значение для struct, соиспользование для class | ref, out, in |
Python и JavaScript
def update(items):
items.append(4) # меняет общий список: вызывающий код это увидит
items = [0] # переназначает локальное имя: снаружи не видно
data = [1, 2, 3]
update(data)
print(data) # [1, 2, 3, 4]
JavaScript ведёт себя так же с объектами и массивами. Строки и числа неизменяемы, поэтому ни в одном из языков функция не может изменить их на месте.
Java
Метод получает копию ссылки. list.add(x) меняет список вызывающего кода, а list = new ArrayList<>() — нет. Заставить Java-метод заменить переменную вызывающего кода невозможно.
Go
Go копирует всё, включая структуры. Чтобы функция могла изменить структуру, передайте указатель:
func rename(u *User) { u.Name = "Ann" }
Срезы и map — это небольшие заголовки, указывающие на общие данные. Изменение s[0] внутри функции видно снаружи, но append может выделить новый массив, и это изменение не будет видно, если не вернуть срез.
C#
Структуры копируются, экземпляры классов — общие. Ключевое слово ref даёт настоящую передачу по ссылке: присваивание ref-параметру меняет переменную вызывающего кода. out нужен для возврата дополнительных значений, а in передаёт ссылку только для чтения, что избавляет от копирования больших структур.
Типичные баги из-за изменения аргументов
- Неожиданные побочные эффекты. Вспомогательная функция «чистит» список, удаляя элементы, и исходные данные, нужные в другом месте, тоже меняются.
- Изменяемые значения по умолчанию в Python. Объект по умолчанию создаётся один раз и общий для всех вызовов:
def add_tag(tag, tags=[]): # баг: один список на все вызовы
tags.append(tag)
return tags
def add_tag(tag, tags=None): # исправление
tags = [] if tags is None else tags
tags.append(tag)
return tags
- Общее состояние в UI. В React и похожих библиотеках изменение состояния на месте не меняет ссылку, и обновление может быть не замечено.
- Сюрпризы с
appendв Go. Два среза могут делить один массив и перезаписывать данные друг друга.
Копирование: поверхностное и глубокое
Если нужен независимый объект, скопируйте его — но понимайте, насколько глубоко идёт копия.
- Поверхностная копия (shallow copy) создаёт новый внешний контейнер, а вложенные объекты остаются общими.
- Глубокая копия (deep copy) рекурсивно копирует всё, ничего общего не остаётся.
| Язык | Поверхностная копия | Глубокая копия |
|---|---|---|
| Python | list(x), x.copy(), copy.copy(x) | copy.deepcopy(x) |
| JavaScript | [...arr], {...obj}, Object.assign | structuredClone(obj) |
| Java | new ArrayList<>(list), конструкторы копирования | Вручную или через сериализацию |
| Go | copy(dst, src), maps.Clone | Вручную |
| C# | MemberwiseClone, new List<T>(list) | Вручную или через сериализацию |
Поверхностная копия списка словарей по-прежнему делит сами словари. Это самый частый источник ситуации «я же скопировал, а оно всё равно изменилось».
Как избежать багов
- Не изменяйте аргументы, если это не очевидно из имени функции (
sort_in_place,appendTo). - Возвращайте новые значения вместо изменения входных — такой код проще тестировать.
- Предпочитайте неизменяемые данные, где язык позволяет: кортежи,
frozenset,readonly, records,Object.freezeдля поверхностной защиты. - Копируйте на границе — когда данные входят в модуль или сохраняются, скопируйте их один раз.
- Делайте глубокую копию осознанно. Она медленнее и может сломаться на объектах с внешними ресурсами, поэтому применяйте её только там, где действительно нужна полная независимость.