Проблема с клонированием и передачей по ссылке

Поэтому в течение последних нескольких дней я разрывал свои волосы, пытаясь получить класс, чтобы клонировать его правильно. Проблема заключается в том, что клонирование не удаляет / не отменяет никакой передачи по ссылке. В результате основной объект данных по-прежнему передается как ссылка, что полностью отрицает эффект клона.

Вот упрощенная версия проблемы:

class my_class { private $data; public $var1; public $var2; public $var3; public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } } $original = new my_class; $new = clone $original; $new->var3 = 'd'; // Output Should Be "c", outputs "d" echo $original->var3; 

Смотрите здесь в действии: http://3v4l.org/nm6NW

Мой вопрос: как я могу использовать __clone() для исправления вывода с «d» на «c»?

Пожалуйста, помогите в любом случае!


Обновить

Я закончил создание ошибки __clone() и создал функцию, называемую make_clone() для стандартизации клонирования.

Теперь __clone() выглядит так:

 public function __clone() { $trace = debug_backtrace(); $fmt = 'Invalid clone in <b>%4$s</b> on line <b>%5$s</b>. Because of how cloning works, and how references are configured within the class, extensions of %1$s cannot be cloned. Please use <code>make_clone()</code> instead. Error triggered'; trigger_error(sprintf($fmt, $trace[0]['class'], $trace[0]['function'], 'clone', $trace[0]['file'], $trace[0]['line']), E_USER_NOTICE); } 

и make_clone() выглядит так:

 public function make_clone() { // This line recreates the current instance at its' current state. $clone = new $this(generate::string($this->object)); // In my class $this->input is a type of history state. // The history of both the original and the clone should match $clone->input = $this->input; return $clone; } 

Solutions Collecting From Web of "Проблема с клонированием и передачей по ссылке"

TL; DR

Это классический случай PHP SNAFU. Я объясню, как и почему это происходит, но, к сожалению, насколько я могу судить, нет приемлемого решения, которое было бы удовлетворительным.

Хрупкое решение существует, если вы можете запускать код до того, как PHP неглубоко клонирует объект (например, путем написания собственного метода клонирования), но не в том случае, если код запускается впоследствии, каким образом работает __clone . Однако это решение может по-прежнему не срабатывать по другим причинам, находящимся вне вашего контроля.

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

В конце концов, если вы хотите сохранить свое здравомыслие, вам придется отказаться от реализации свойств $this->varN качестве ссылок.

Тяжелое положение бедного PHP-разработчика

Обычно вам придется глубоко клонировать все, что нужно клонировать внутри __clone . Затем вам также придется переназначить любые ссылки, которые все еще указывают на экземпляры, которые были просто клонированы.

Вы бы подумали, что этих двух шагов должно быть достаточно, например:

 public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function __clone() { $this->data = clone $this->data; $this->assignReferences(); } private function assignReferences() { $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } 

Однако это не работает . Как это может быть?

Ссылки на двигатель Zend

Если вы var_dump($this->data) до и после assignReferences() в конструкторе, вы увидите, что назначение этих ссылок заставляет содержимое $this->data стать самими ссылками .

Это артефакт о том, как ссылки внутренне реализованы в PHP, и вы ничего не можете с этим сделать напрямую. То, что вы можете сделать, это преобразовать их обратно к нормальным значениям сначала, потеряв все другие ссылки на них, после чего будет работать клонирование, как описано выше.

В коде:

 public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function makeClone() { unset($this->var1); // turns $this->data->var1 into non-reference unset($this->var2); // turns $this->data->var2 into non-reference unset($this->var3); // turns $this->data->var3 into non-reference $clone = clone $this; // this code is the same $clone->data = clone $clone->data; // as what would go into $clone->assignReferences(); // __clone() normally $this->assignReferences(); // undo the unset()s return $clone; } private function assignReferences() { $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } с public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function makeClone() { unset($this->var1); // turns $this->data->var1 into non-reference unset($this->var2); // turns $this->data->var2 into non-reference unset($this->var3); // turns $this->data->var3 into non-reference $clone = clone $this; // this code is the same $clone->data = clone $clone->data; // as what would go into $clone->assignReferences(); // __clone() normally $this->assignReferences(); // undo the unset()s return $clone; } private function assignReferences() { $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } с public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function makeClone() { unset($this->var1); // turns $this->data->var1 into non-reference unset($this->var2); // turns $this->data->var2 into non-reference unset($this->var3); // turns $this->data->var3 into non-reference $clone = clone $this; // this code is the same $clone->data = clone $clone->data; // as what would go into $clone->assignReferences(); // __clone() normally $this->assignReferences(); // undo the unset()s return $clone; } private function assignReferences() { $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } с public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function makeClone() { unset($this->var1); // turns $this->data->var1 into non-reference unset($this->var2); // turns $this->data->var2 into non-reference unset($this->var3); // turns $this->data->var3 into non-reference $clone = clone $this; // this code is the same $clone->data = clone $clone->data; // as what would go into $clone->assignReferences(); // __clone() normally $this->assignReferences(); // undo the unset()s return $clone; } private function assignReferences() { $this->var1 = &$this->data->var1; $this->var2 = &$this->data->var2; $this->var3 = &$this->data->var3; } 

Это похоже на работу , но это сразу не очень удовлетворительно, потому что вы должны знать, что способ клонирования этого объекта – $obj->makeClone() вместо clone $obj – естественный подход не удастся.

Однако есть и более коварная ошибка, ожидающая вас, чтобы укусить вас: чтобы не ссылаться на значения внутри $this->data вы должны потерять все ссылки на них в программе. Вышеприведенный код делает это для ссылок в $this->varN , но как насчет ссылок на другой код ?

Сравните это:

 $original = new my_class; $new = $original->makeClone(); $new->var3 = 'd'; echo $original->var3; // works, "c" 

К этому:

 $original = new my_class; $oops = &$original->var3; // did you think this might be a problem? $new = $original->makeClone(); $new->var3 = 'd'; echo $original->var3; // doesn't work! 

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

Убейте ссылки огнем

Существует гарантированный способ заставить ссылки внутри $this->data уйти независимо от того, что: сериализация.

 public function __construct() { $this->data = new stdClass; $this->data->var1 = 'a'; $this->data->var2 = 'b'; $this->data->var3 = 'c'; $this->assignReferences(); } public function __clone() { $this->data = unserialize(serialize($this->data)); // instead of clone $this->assignReferences(); } 

Это работает со значениями, о которых идет речь, но имеет и недостатки:

  1. У вас не может быть значений (рекурсивно) внутри $this->data , которые не могут быть сериализованы.
  2. Он будет без разбора убивать все ссылки внутри $this->data – даже те, которые вы, возможно, захотите сохранить нарочно.
  3. Это менее результативно (теоретически, справедливо).

Так что делать?

После обязательного избиения PHP следуйте совету классического врача: если больно, когда вы что-то делаете, тогда не делайте этого.

В этом случае это означает, что вы просто не можете выставлять содержимое $this->data через общедоступные свойства (ссылки) на объект. Вместо этого используйте функции getter или, возможно, реализуйте магический __get .