Ссылки в PHP

16 июля 2018
Официальная документация: https://www.php.net/manual/ru/language.references.php
Подсчёт ссылок для сборки мусора: https://www.php.net/manual/ru/features.gc.refcounting-basics.php

Введение

Ссылки – это механизм для доступа к значению переменной, с помощью которого несколько переменных-ссылок могут закрепляться за одним участком памяти. Связь ссылки и участка памяти организовано ядром PHP через специальные таблицы имён (Symbol table), которые создаются отдельное для каждой области видимости: глобально, на уровне скрипта, а так же для каждой функции. В таблице имён хранится название переменной и указатель на ячейку памяти. В ячейке памяти хранится структура типа zval, которая состоит из полей: value, type, refcount__gc, is_ref__gc. Описание структуры:
typedef struct _zval_struct {
    zvalue_value value;			/* значение переменной */
    zend_uint refcount__gc;		/* количество переменных, ссылающихся на участок памяти */
    zend_uchar type;			/* тип значения */
    zend_uchar is_ref__gc;		/* флаг "это ссылка" */
} zval;

Если поле is_ref__gc содержит 1 (true), то переменная является ссылкой. Но тут есть тонкий момент – если на переменную ссылается какая-нибудь другая переменная, то исходная переменная тоже становится ссылкой. Это можно проверить через расширение Xdebug с помощью функции xdebug_debug_zval
$a = 'DDD';
xdebug_debug_zval('a'); /* Выведет a: (refcount=2, is_ref=0)string 'DDD' (length=3) */

$b = &$a; /* Объявление ссылки $b */
$a = 'AAA';
xdebug_debug_zval('a'); /* Выведет a: (refcount=2, is_ref=1)string 'AAA' (length=3) */

unset($b);
xdebug_debug_zval('a'); /* Выведет a: (refcount=1, is_ref=1)string 'AAA' (length=3) */

Поле refcount__gc указывает сколько переменных используют участок памяти. Если на переменную никто не ссылается, то поле будет иметь значение 1, так как ячейку памяти использует только сама переменная. В случае, когда на переменную будут ссылаться, например, 2 другие переменные, то поле refcount__gc будет содержать значение 3. Когда refcount__gc становится равным нулю, область памяти переменной (структура zval) освобождается сборщиком мусора.

Следующий код выведет refcount=2, хотя по задумке refcount должен иметь значение 1. Это происходит из-за оптимизации управления памятью в PHP, когда система передаёт аргумент в функцию через ссылку, а так как ссылки у переменной нету, то она создаётся и таким образом в области видимости функции переменная имеет refcount=2. Подробнее о таком поведении можно почитать в документации https://www.php.net/manual/ru/function.debug-zval-dump.php
$a = 'DDD';
xdebug_debug_zval('a'); /* Выведет a: (refcount=2, is_ref=0)string 'DDD' (length=3) */

Объявление ссылки

Ссылка объявляется с помощью оператора &
$a = null;
$b = &$a; /* Теперь значение переменной-ссылки $b будет адресоваться по тому же участку памяти, что и значение $a */
$a = 'AAA';
echo $b; /* Выведет 'ААА' */

Сброс связи ссылки и значения переменной происходит с помощью функции unset, при этом происходит именно удаление связи, а не значения. Участок памяти освобождается, когда на него не адресуется ни одна переменная (refcount=0)
$a = 'DDD';
$b = &$a; /* Переменная $b стала ссылаться на значение $a, но и $a стала ссылкой на значение */

unset($a); /* Сбросили связь */
$a = 'EEE'; /* Так как $a перестала куда-либо ссылаться, то при присвоении нового значения на это выделяется новый участок памяти */

echo $a; /* Выведет 'EEE' */
echo $b; /* Выведет 'DDD' */

Иногда пробуют сбрасывать связь присваиванием значения null, но так делать нельзя, т.к операция присвоения присваивает значение.
$a = 'DDD';
$b = &$a;
$b = null; /* null только сбросит значение, а связь ссылки и ячейки памяти сохранится */

var_dump($a); /* Выведет 'NULL' */
var_dump($b); /* Выведет 'NULL' */

$a = 'DDD';

var_dump($a); /* Выведет 'string(3) "DDD"' */
var_dump($b); /* Выведет 'string(3) "DDD"' */

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

Для передачи в функцию параметра по ссылке используется оператор &, который указывается перед аргументом при объявлении функции.
/* Функция прибавляет к метке времени 1 день */
function add_day_to_timestamp(&$timestamp)
{
	$timestamp = $timestamp + 86400;
	return true;
}

$current_ts = date('U'); /* Получаем текущую метку времени */
echo date('d.m.Y H:i:s', $current_ts); /* Выведет текущую дату (16.07.2018 09:02:55) */
echo '<br>';

add_day_to_timestamp($current_ts); /* Увеличиваем метку времени на одни сутки */
echo date('d.m.Y H:i:s', $current_ts); /* Выведет текущую дату, увеличенную на 1 сутки, т.е '17.07.2018 09:02:55' */

Возврат значения функции по ссылке

Для возврата значения функции по ссылке используется оператор &, который указывается в двух местах:
  1. Перед названием функции в момент её объявления;
  2. После оператора =, когда присваивается значение функции.
$GLOBALS['current_day'] = date('d.m.Y'); /* Текущая дата '16.07.2018' */

function &get_global_var($var)
{
	$var = trim((string)$var);
	if($var=='') exit('Ошибка. У функции get_global_var в параметре $var указано пустое значение');
	else
	{
		if(isset($GLOBALS[$var])==false) $GLOBALS[$var] = '';
		return $GLOBALS[$var]; /* Возврат значения (ссылки на значение) */
	}
}

$a = &get_global_var('current_day'); /* Теперь $a указывает на ячейку значения, как и $GLOBALS['current_day'] */
echo $a; /* Выведет '16.07.2018' */
echo '<br>';

$GLOBALS['current_day'] = '10.07.2018';
echo $a; /* Выведет '10.07.2018' */
echo '<br>';

$a = '12.07.2018';
echo $GLOBALS['current_day']; /* Выведет '12.07.2018' */

Особенности языка

  • При объявлении глобальных переменных с помощью инструкции GLOBAL создаются не отдельные самостоятельные переменные, а ссылки, указывающие на элемент глобального массива $GLOBALS;
  • В классах ключевое слово $this является ссылкой на созданный экземпляр класса (ссылкой на объект).