• Блог
  • Копирование объектов в 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. Создаётся объект в хэш-таблице.
                             // 2. Создаётся контейнер №1 со ссылкой на созданный объект.
$reference =& $instance;     // 3. Переменная $reference добавляется к контейнеру №1.
$assignment = $instance;     // 4. Создаётся контейнер №2, который является копией контейнера №1. 

$reference = NULL; // Изменим переменную контейнера №1
// Или так
//$reference = 'Строка';
// Или так
//$reference = 123;

Теперь значение контейнера №1 вместо ссылки на объект хранит значение NULL (или другое значение). Но в контейнере №2 всё ещё хранится ссылка. Поэтому обращаясь к переменной $assignment мы по этой ссылке получаем объект.

Кстати, счётчик refcount мониторится сборщиком мусора. Если он становится равен 0, то сборщик мусора освобождает память, занимаемую данным контейнером, так как нет переменных, которые на него ссылаются.

Заключение

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

0   4466

Комментарии ()

    Вы должны авторизоваться, чтобы оставлять комментарии.

    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.