Данную статью меня побудил написать один интересный момент с объектами, который многие новички, освоив азы языка PHP, до конца не понимают. Давайте рассмотрим следующий код:
class SomeClass{ public $foo="bar"; }; $instance = new SomeClass(); // Создаём объект $reference =& $instance; // Создаём ссылку на объект $assignment = $instance; // Новой переменной присваиваем созданный объект // Обнуляем ссылку $reference = null; // Выводим переменные var_dump($instance); var_dump($reference); var_dump($assignment);
Получаем вот такой результат:
NULL NULL object(SomeClass)#1 (1) { ["foo"]=> string(3) "bar" }
Для новичка в PHP непонятно, что здесь не так — ведь обычные переменные ведут себя также. Т.е. если $instance
присвоить не объект, а, например, строку, то результат будет такой же. А вот у программиста, уже познакомившегося в объектами, возникает вопрос — а почему переменная $assignment
не обнулилась? Ведь они то знают, что правило присваивания для объектов несколько отличается от правила для обычных переменных. Давайте попробуем разобраться. Начнём с примера для обычных переменных:
$var1 = 'Строка'; $var2 = $var1; // Создаётся копия переменной $var1 // Изменим значение второй переменной $var1 = 'Новая строка'; echo $var1; // Строка echo $var2; // Новая строка
Как видите, это две разные переменные, не связанные друг с другом. Но с объектами всё по другому.
class SomeClass { $foo = 'Строка'; } $object1 = new SomeClass(); $object2 = $object1; // Изменим свойство у второй переменной $object2->foo = 'Новая строка'; var_dump($object1); var_dump($object2); // Результат object(SomeClass)#1 (1) { ["foo"]=> string(23) "Новая строка" } object(SomeClass)#1 (1) { ["foo"]=> string(23) "Новая строка" }
И мы видим, что у первой переменной значение свойства foo
также изменилось. И ответом является то, что и первая и вторая переменная ссылаются на один и тот же объект с #1. А вот теперь вернитесь к коду в начале статьи и задумайтесь — переменная $assignment
ссылается на тот же объект $instance
, но её значение не меняется!
Немного теории
А что же такого особенного в объектах и почему разработчики PHP изменили логику работы с ними? А всё дело в конструкции new
при операции присваивания (=). Если использовать логику присваивания как для обычных переменных, то такая конструкция
$instance = new SomeClass();
создаёт 2 объекта — сначала создаётся безымянный объект new SomeClass()
, а затем создаётся копия в переменной $instance
. И в памяти будут висеть 2 объекта. Поэтому для объектов операцию присваивания изменили и в переменную $instance
передаётся не сам объект, а ссылка на него. Т.е. присваивания как такого нет. А для копирования объекта разработчики добавили специальную инструкцию clone.
Вернёмся к нашему примеру
Так почему же переменная $assignment
не изменилась? Ведь теперь мы знаем, что она ссылается на тот же объект, что и переменная $instance
. Чтобы понять это, обратимся к ядру PHP — Zend Engine.
При создании переменной в памяти выделяется блок для её хранения. Сама переменная представлена в виде контейнера zval (Zend value):
struct _zval_struct { /* Тип хранимого значения в value */ zend_uchar type; /* Хранимое значение */ zvalue_value value; /* Счетчик ссылок переменных*/ zend_uint refcount; /* Флаг ссылки */ zend_uchar is_ref; };
Вот так будет выглядеть строковая переменная $foo = "bar"
:
foo: { type: string, value: str: val: "bar" len: 3 is_ref: 0 refcount: 1 }
А если добавить ссылку на эту переменную $xyz =& $foo
, то получим следующую структуру:
xyz,foo: { type: string, value: str: val: "bar" len: 3 is_ref: 1 // Признак ссылки refcount: 2 // Количество переменных, ссылающихся на этот контейнер }
Как видим контейнер остался тот же, но теперь им «владеют» 2 переменные. Это указано в свойстве refcount
. А флаг is_ref
теперь имеет значение 1, так как обе переменные являются ссылками на один и тот же контейнер. В случае же обычного присваивания для новой переменной создаётся отдельный контейнер, который является копией первого.
На самом деле в целях оптимизации памяти копирование происходит только при изменении одной из переменных, а пока они равны обе переменные ссылаются на одну область памяти. Эта техника называется copy on write
.
При создании объекта для него также создается отдельный контейнер с типом object, но в качестве значения в нём хранится идентификатор объекта, а сам объект создаётся и хранится в специальной таблице. Это ключевой момент. Т.е. при присваивании объекта другой переменной Zend Engine оперирует не самим объектом, а его идентификатором, указанным в контейнере zval. Поэтому для этой операции действуют точно такие же правила как и для переменных. Теперь давайте вернёмся к первоначальному примеру:
class SomeClass{ public $foo="bar"; }; $instance = new SomeClass(); // Создаётся контейнер №1 для объекта класса SomeClass. $reference =& $instance; // Переменная $reference добавляется к контейнеру №1. $assignment = $instance; // Создаётся контейнер №2, который является копией контейнера №1. $reference = NULL; // Изменим переменную контейнера №1 // Или так //$reference = 'Строка'; // Или так //$reference = 123;
Теперь значение контейнера №1 вместо ссылки на объект хранит значение NULL (или другое значение). Но в контейнере №2 всё ещё хранится ссылка. Поэтому обращаясь к переменной $assignment
мы по этой ссылке получаем объект.
Кстати, счётчик refcount мониторится сборщиком мусора. Если он равен 0 (при удалении переменной методом unset()
), то сборщик мусора освобождает память, занимаемую данным контейнером, так как нет переменных, которые на него ссылаются.
Заключение
Статья получилась достаточно большая и не очень простая, но всё-таки постарайтесь вникнуть в неё, так как эта информация важна для понимания механизма работы с объектами и ссылками.