Перейти к содержанию
Innovialabs

Передача по значению и по ссылке: как работают аргументы функций

Основы4 мин

Коротко

Большинство популярных языков передают копию значения, а для объектов это значение — ссылка: функция может изменить общий объект, но не может переназначить переменную вызывающего кода; настоящая передача по ссылке есть только там, где язык это явно позволяет, например ref в C# или указатели в Go.

Короткий ответ

Встречаются три модели:

  • Передача по значению — функция получает копию. Её изменение не влияет на вызывающий код.
  • Передача по ссылке — функция получает псевдоним переменной вызывающего кода. Присваивание параметру меняет саму эту переменную.
  • Передача по соиспользованию (call by sharing, «ссылка передаётся по значению») — функция получает копию ссылки на тот же объект. Она может изменить объект, но переприсваивание параметра меняет только локальную копию.

Python, JavaScript и Java для объектов используют третью модель. Поэтому популярное утверждение «Java передаёт по ссылке» неверно.

По языкам

ЯзыкПо умолчаниюНастоящая передача по ссылке
PythonСоиспользование (всё — объекты)Нет
JavaScriptЗначение для примитивов, соиспользование для объектовНет
JavaЗначение для примитивов, соиспользование для объектовНет
GoВсегда значение; срезы и map содержат внутренние указателиУказатели (*T)
C#Значение для struct, соиспользование для classref, 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) рекурсивно копирует всё, ничего общего не остаётся.
ЯзыкПоверхностная копияГлубокая копия
Pythonlist(x), x.copy(), copy.copy(x)copy.deepcopy(x)
JavaScript[...arr], {...obj}, Object.assignstructuredClone(obj)
Javanew ArrayList<>(list), конструкторы копированияВручную или через сериализацию
Gocopy(dst, src), maps.CloneВручную
C#MemberwiseClone, new List<T>(list)Вручную или через сериализацию

Поверхностная копия списка словарей по-прежнему делит сами словари. Это самый частый источник ситуации «я же скопировал, а оно всё равно изменилось».

Как избежать багов

  • Не изменяйте аргументы, если это не очевидно из имени функции (sort_in_place, appendTo).
  • Возвращайте новые значения вместо изменения входных — такой код проще тестировать.
  • Предпочитайте неизменяемые данные, где язык позволяет: кортежи, frozenset, readonly, records, Object.freeze для поверхностной защиты.
  • Копируйте на границе — когда данные входят в модуль или сохраняются, скопируйте их один раз.
  • Делайте глубокую копию осознанно. Она медленнее и может сломаться на объектах с внешними ресурсами, поэтому применяйте её только там, где действительно нужна полная независимость.

Частые вопросы

Нет. Java всегда передаёт по значению. Для объектов значение — это ссылка, поэтому метод может менять поля объекта, но не может заставить переменную вызывающего кода указывать на другой объект.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.