Найти в Дзене
ZDG

Новичку-программисту: Передача параметров по ссылке

Предыдущие части: Что передаётся в функцию, Как не запутаться в именах-2, Как не запутаться в именах-1

В предыдущей части мы рассмотрели, как происходит передача значения в функцию. Это была передача по значению. Функция получает копию значения, и даже если она поменяет её, оригинальное значение не изменится. Есть другой вариант – передача по ссылке.

Но чтобы ничего не переусложнять, давайте сразу определимся: передача в функцию или не передача, это для самого механизма значений или ссылок никакой роли не играет, потому что он используется в программе повсеместно и без всяких функций. И нас интересует не передача, а именно этот механизм. Сейчас мы его рассмотрим ещё раз, для лучшего понимания, и тогда уже разберёмся с функциями.

Имена переменных, задолго до программирования, использовались в математике. Математика это абстрактная наука. Можно назвать именем "a" некую константу 5, или целую матрицу, или даже систему уравнений.

И хотя программный код довольно близко копирует математический подход, есть одно различие: все переменные имеют физическое воплощение. Они не абстрактны.

Каждая переменная должна быть физически размещена в памяти. Это значит, что она получает адрес в памяти, о котором мы, как правило, не знаем. Мы знаем только имя переменной.

Переменная с именем "a" это физическая ячейка памяти с адресом, например, 1008.
Переменная с именем "a" это физическая ячейка памяти с адресом, например, 1008.

Если в математике мы можем сказать, что "a" это матрица, и "a" будет матрицей, то в программировании всё несколько сложнее. Так как переменная физически существует в памяти, это значит, что она является самостоятельным объектом, отдельно от своего значения.

Любая переменная занимает в памяти не более чем, ну скажем, 8 байт (если система 64-битная). Из этого следует, что в ту ячейку памяти, которая назначена этой переменной, мы можем поместить какое-то число.

Если оно туда помещается целиком, то мы можем говорить, что в переменной хранится некое значение. И к этому значению мы можем обращаться непосредственно. Это всё равно что у вас есть коробка, в коробке что-то лежит, и засунув туда руку, вы можете непосредственно пощупать содержимое.

-2

Теперь рассмотрим более сложные структуры, например массивы или объекты. Они состоят из множества байт, которые не помещаются в максимальный размер ячейки памяти.

Но присваиваем-то мы их всё равно одной переменной. А эта переменная – по-прежнему одна ячейка памяти ограниченного размера. Каким образом большой объём данных помещается в эту ячейку?

Ответ: никаким. Весь этот объём данных хранится где-то в совершенно отдельном месте. Но у него есть начало, то есть первый байт. Адрес этого байта и помещается в ячейку-переменную.

Предположим, мы решили присвоить переменной "a" массив из 5 элементов: [1,2,3,4,5]. Эти элементы будут размещены где-то в памяти. Но адрес первого элемента будет сохранён в ячейке "a".

Адрес первого элемента массива (1000) записан в ячейку "a"
Адрес первого элемента массива (1000) записан в ячейку "a"

То есть, если мы раньше засовывали руку в коробку и могли пощупать непосредственно содержимое коробки, то теперь мы можем пощупать лишь записку, которая в ней лежит. И в этой записке написано: смотри адрес такой-то.

-4

И тогда мы переходим на тот адрес, который указан в записке, и что мы там видим? Первый байт массива.

Получается, что переменная, которой мы присвоили 5, хранит значение 5. А переменная, которой мы присвоили массив, хранит не сам массив, а его адрес – то есть ссылку, или указатель на него. Вот это и есть обращение по ссылке. В данном случае мы можем видеть, как переменная и массив существуют отдельно друг от друга, являясь разными сущностями.

Прямые значения и ссылки

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

Вообще говоря, напрямую управлять ссылками мы можем только в языках низкого уровня, таких как C. Управление памятью там полностью прозрачно, и поэтому мы точно знаем, где нужна ссылка, а где прямое значение. И описываем переменные соответственно, в явном виде.

В языках с автоматическим менеджментом памяти, например, PHP, Python, Java, JavaScript, детали создания переменных от нас скрыты, поэтому то, что мы видим, может нас обмануть. Скажем, такая простая инструкция:

a = 5

всем своим видом показывает, что значение "5" записано в ячейку памяти "а" напрямую. Теоретически же вместо значения "5" в памяти может быть создан целый объект. Где-то в свойствах этого объекта будет записано "5". И адрес этого объекта будет записан в ячейку "a". То есть вместо прямого значения мы получили ссылку.

Нас это, однако, волновать не должно. Мы должны смотреть на семантику инструкций. Если инструкция выглядит как a = 5, то работать эта переменная должна так, как будто мы присвоили ей прямое значение.

А подобная инструкция:

a = new Object()

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

Посмотрим пример на Java:

-5

Разберём построчно. Мы создали целочисленный массив "a" из элементов 1,2,3,4,5. При этом в памяти была выделена область под массив, эта область была заполнена значениями 1,2,3,4,5, а адрес начала области был записан в "a". Получилась ссылка. То же самое произошло, когда мы создали массив "b" из элементов 10,20,30,40,50.

Далее, когда мы написали a = b, что чему присвоилось? В переменной "a" хранился адрес массива [1,2,3,4,5]. После присвоения в переменной "a" стал храниться адрес массива [10,20,30,40,50], скопированный из "b". То есть теперь мы имеем переменные "a" и "b", в которых хранится один и тот же адрес, и обе этих переменные указывают на один и тот же массив. Что случилось с первым массивом? На него больше никакая переменная не указывает, поэтому сборщик мусора освободит память, которую он занимал.

Мы печатаем для проверки элемент массива a[2] и видим число 30, то есть действительно переменная "a" указывает на тот же массив, что и "b".

Передача в функцию

Далее мы передаём переменную "a" в функцию test(). Эта функция изменяет элемент массива a[2], присваивая ему значение 200. Размещу картинку ещё раз, чтобы за ней не подниматься:

-6

После возвращения из функции мы снова печатаем значение a[2], и на этот раз оно равно 200. То есть изменение, сделанное внутри функции, отразилось на переданной переменной. Почему это произошло? Ведь в прошлом выпуске мы выяснили, что в функцию передаётся только копия значения переменной, а саму переменную изменить нельзя?

Так ведь мы и не меняли саму переменную. Переменная и массив, как я пояснял выше – физически не одно и то же. Значением переменной "a" является адрес массива, копия этого адреса и была передана в функцию. Мы не трогали этот адрес, он каким был, таким и остался. Вместо этого мы перешли по адресу и изменили элемент, который там хранится со смещением 2. Мы просто обращаемся к переменной, но так как эта переменная – указатель, то по указателю мы попадаем на целевой объект, и так как это уже не копия, а сам объект, то мы можем менять его данные откуда угодно.

Для верности мы также распечатываем значение b[2]. И оно тоже равно 200, хотя переменную "b" мы нигде не изменяли и никуда не передавали. Очевидно, так как "a" и "b" указывают на один и тот же массив, то изменив массив через ссылку "a", мы увидим те же самые изменения через ссылку "b".

Это, кстати, надёжный способ понять, что вы имеете дело со ссылками. Если при изменении одной переменной меняется другая, это значит, что обе они – ссылки, и указывают на один и тот же объект. В случаях, когда вы в чём-то не уверены, вы можете написать короткий проверочный код, подобный размещённому выше, и убедиться, что всё работает (или не работает) так, как вы представляете.

Осталось разобраться – если переменная может хранить и прямое значение, и адрес, и это просто числа, то как программный код понимает, когда нужно использовать содержимое ячейки переменной напрямую, а когда считать его адресом?

В типизированных языках это элементарно. Сам тип переменной указывает на то, чем она является. В вышеприведённом примере на Java переменная имеет тип int[], то есть "целочисленный массив", и значит она является указателем. Об этом в курсе и функция, куда мы передаём переменную – её параметр тоже описан как int[].

В таких языках, как Python, PHP и JavaScript, переменные не имеют типа (хотя в новых версиях уже местами можно применять типы, но это необязательно).

Тем не менее, информация о переменной сохраняется в служебных, скрытых от нас данных. То есть, если мы написали такое присваивание:

a = 5

То информация о переменной будет сохранена как "это переменная с прямым значением, и в данный момент это целое число".

Если же потом ту же самую переменную мы переприсваиваем с совершенно другим типом (что возможно в нетипизированных языках), например

a = [1,2,3,4,5]

То будет отдельно выделена память под массив, и его адрес будет записан в переменную, а служебная информация поменяется и теперь будет гласить "это переменная-указатель, и в данный момент она указывает на массив".

Если передать такую переменную в функцию, то несмотря на то, что функция ничего не о ней не знает (параметр без типа, и значит можно передать что угодно), запись a[2] = 200 указывает на то, что переменная используется как ссылка на массив. И если служебная информация о переменной совпадает с таким допущением, значит всё пройдёт как надо и мы будем работать именно со ссылкой на массив.

Оговорки и исключения

Я описал общую ситуацию, но в каждом языке возможны нюансы. Как бы то ни было, эти нюансы никак не нарушают принципов работы со ссылками, а просто используют другие соглашения. В некоторых случаях, например, массив может передаваться в функцию не по ссылке, а полностью копироваться. То есть копируется не значение переменной с адресом массива, а весь массив целиком, и передаётся ссылка на копию. Естественно, по ссылке на копию мы можем изменить лишь копию, что не затронет оригинальный массив.

Такая практика есть, например, в PHP. Она касается только массивов. То есть объекты передаются по ссылке, и их можно менять. А вот массивы копируются целиком. Зачем это было сделано, сказать затрудняюсь. Но в PHP есть способ передать не только массив, но даже и простую переменную по ссылке. Для этого нужно перед параметром функции написать "&" (аналогично тому, как в C). Это делает из любой переменной ссылку:

-7

Проверим-ка Python:

-8

И... передача массива работает по ссылке.

А что с JavaScript?

-9

И тоже работает!

В общем, изучите язык, на котором пишете. Если вам лень читать документацию, то хотя бы проведите вот такие тесты, чтобы не оставалось никаких сомнений.