【问题标题】:Issue with cloning and pass-by-reference克隆和引用传递问题
【发布时间】:2014-10-14 18:15:49
【问题描述】:

所以在过去的几天里,我一直在努力让课程正确克隆。问题是克隆不会删除/重做任何传递引用。结果是,主数据对象仍然作为引用传递,从而完全否定了克隆的效果。

这是问题的简化版本:

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;
}

【问题讨论】:

  • 标准的clone 方法执行浅拷贝。听起来你需要一个深拷贝,所以你需要你的 clone 方法来克隆$this-&gt;object
  • 试过了,还是不行
  • 我不确定这与传递引用有什么关系。这仅适用于您调用函数时。
  • 你能做一个简化的测试用例,你可以在这里发布。试图从 github 上的数百行代码中弄清楚你在做什么是很困难的。此外,一旦问题得到解决并且您更新了 github,这些链接将失效。
  • @barmar 至于传递引用,$a = &amp;$b 与 `$a = $b' 不同。引用传递不限于函数/方法。

标签: php oop clone pass-by-reference


【解决方案1】:

TL;DR

这是一个典型的 PHP SNAFU 案例。我将解释它是如何发生的以及为什么会发生,但不幸的是,据我所知,没有令人满意的解决方案。

如果您可以在 PHP 浅层克隆对象之前运行代码(例如,通过编写自己的克隆方法),则存在 脆弱 解决方案,但如果代码在之后运行,则不会存在,这就是 __clone 的工作方式.但是,此解决方案仍可能因您无法控制的其他原因而失败。

还有另一个安全的选项,它涉及一个众所周知的“克隆”技巧,但它也有缺点:它只适用于可序列化的数据,并且不允许您在其中保留任何引用数据,即使你想。

归根结底,如果您想保持理智,您将不得不放弃将属性 $this-&gt;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;        
}

但是,this does not work。怎么可能?

Zend 引擎参考

如果您在构造函数中在assignReferences() 之前和之后var_dump($this-&gt;data),您将看到分配这些引用会导致$this-&gt;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;        
}

This appears to work,但它立即不是很令人满意,因为你必须知道克隆这个对象的方式是$obj-&gt;makeClone()而不是clone $obj——自然方法会失败。

然而,这里还有一个更隐蔽的 bug 等着你:要取消引用 $this-&gt;data 中的值,你必须在程序中丢失对它们的所有引用。上面的代码对$this-&gt;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!

We are now back to square one。更糟糕的是,没有办法防止有人这样做并弄乱你的程序。

用火杀死引用

有一种保证方法可以让$this-&gt;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();
}

This works 有问题的值,但它也有缺点:

  1. $this-&gt;data 内不能有任何不可序列化的值(递归)。
  2. 它会不加选择地杀死 $this-&gt;data 中的所有引用——甚至是那些你可能想要故意保留的引用。
  3. 性能较差(公平地说,这是理论上的观点)。

那该怎么办?

在强制抨击 PHP 之后,请遵循经典医生的建议:如果你做某事时很痛,那就不要这样做。

在这种情况下,这意味着您不能通过对象上的公共属性(引用)公开$this-&gt;data 的内容。取而代之的是使用 getter 函数或可能实现魔术 __get

【讨论】:

  • 一点建议:在这种情况下,最好使用json_encode/json_decode,因为它是about twice as fast,并且可以达到相同的目标。 Like this
  • @NickJ:IMO 速度比其他可能出现的问题要小得多,例如json_encode 将拒绝无效的 UTF-8 字符串,其中几乎包括所有二进制数据。例如,如果 $this-&gt;var1 = "\xff" 则克隆将失败。为什么要冒险?
  • 考虑到真实类的目的和它可能输入的范围,这可能是一个重要因素。但是,在不久的将来,该类将支持将输入/输出字符串标准化为特定格式(可能是 UTF-8)。我会假设 PHP 的字符串翻译(就像你上面提到的)会克服这个问题???
  • @NickJ:我不确定我是否理解正确,您是在问将字符串内容标准化为 UTF-8 是否可以解决问题?它会解决这个问题,但其他可能性仍然存在(例如,往返数组,很容易发生它作为stdClass 对象返回,这将是一个问题)。例如数组[1 =&gt; 'foo'] 默认作为对象返回。
  • unserialize(serialize(...)) 为我工作。谢谢!
猜你喜欢
  • 1970-01-01
  • 2010-11-20
  • 2021-08-30
  • 1970-01-01
  • 2010-12-18
  • 2011-03-08
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
相关资源
最近更新 更多