Копирование объектов в PHP

Данную статью меня побудил написать один интересный момент с объектами, который многие новички, освоив азы языка 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()), то сборщик мусора освобождает память, занимаемую данным контейнером, так как нет переменных, которые на него ссылаются.

Заключение

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