我的PHP数组引用“神奇地”成为一个值数组......为什么?

时间:2011-12-01 03:50:57

标签: php arrays object mysqli reference

我正在创建一个围绕mysqli的包装函数,这样我的应用程序就不必过于复杂了数据库处理代码。部分原因是使用mysqli :: bind_param()参数化SQL调用的一些代码。你可能知道bind_param()需要引用。由于它是一个半通用的包装器,我最终打了这个电话:

call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);

我收到一条错误消息:

Parameter 2 to mysqli_stmt::bind_param() expected to be a reference, value given

上面的讨论是为了那些会说“你的例子中根本不需要参考文献”的人。

我的“真实”代码比任何人想读的都复杂一点,所以我把导致这个错误的代码煮成了以下(希望)说明性的例子:

class myclass {
  private $myarray = array();

  function setArray($vals) {
    foreach ($vals as $key => &$value) {
      $this->myarray[] =& $value;
    }
    $this->dumpArray();
  }
  function dumpArray() {
    var_dump($this->myarray);
  }
}

function myfunc($vals) {
  $obj = new myclass;
  $obj->setArray($vals);
  $obj->dumpArray();
}

myfunc(array('key1' => 'val1',
             'key2' => 'val2'));

问题似乎是,在myfunc()中,在调用setArray()和调用dumpArray()之间,$ obj-> myarray中的所有元素都不再是引用而是变为值。通过查看输出可以很容易地看出这一点:

array(2) {
  [0]=>
  &string(4) "val1"
  [1]=>
  &string(4) "val2"
}
array(2) {
  [0]=>
  string(4) "val1"
  [1]=>
  string(4) "val2"
}

请注意,此处输出的前半部分中的数组处于“正确”状态。如果这样做是有意义的,那么我可以在那时调用bind_param(),它会起作用。不幸的是,在输出的后半部分出现了问题。注意缺少“&”关于数组值类型。

我的推荐发生了什么?我怎样才能防止这种情况发生?当我真的不是语言专家时,我讨厌称“PHP bug”,但这可能是一个吗?这对我来说似乎很奇怪。我目前正在使用PHP 5.3.8进行测试。


编辑:

正如不止一个人指出的那样,修复方法是更改​​setArray()以通过引用接受其参数:

function setArray(&$vals) {

我正在添加此注释以记录为什么这似乎有效。

一般来说,PHP,尤其是mysqli,似乎对“引用”的概念略显奇怪。观察这个例子:

$a = "foo";
$b = array(&$a);
$c = array(&$a);
var_dump($b);
var_dump($c);

首先,我确定你想知道我为什么使用数组而不是标量变量 - 这是因为var_dump()没有显示标量是否是引用的任何指示,但它确实是阵列成员。

无论如何,此时,$ b [0]和$ c [0]都是对$ a的引用。到现在为止还挺好。现在我们将第一把扳手投入工作:

unset($a);
var_dump($b);
var_dump($c);

$ b [0]和$ c [0]仍然是对同一事物的引用。如果我们换一个,两者都会改变。但是他们提到了什么?内存中有一些未命名的位置。当然,垃圾收集确保我们的数据是安全的,并且将保持不变,直到我们停止引用它为止。

对于我们的下一个技巧,我们这样做:

unset($b);
var_dump($c);

现在$ c [0]是我们数据的唯一剩余参考。而且,哇!奇迹般地,它不再是“参考”。不是通过var_dump()的度量,也不是通过mysqli :: bind_param()的度量。

PHP manual表示每个数据上都有一个单独的标记'is_ref'。但是,此测试似乎表明'is_ref'实际上相当于'(refcount> 1)'

为了好玩,您可以按如下方式修改此玩具示例:

$a = array("foo");
$b = array(&$a[0]);
$c = array(&$a[0]);

var_dump($a);
var_dump($b);
var_dump($c);

请注意,所有三个数组都在其成员上有引用标记,这支持了'is_ref'在功能上等同于'(refcount> 1)'的想法。

为什么mysqli :: bind_param()会首先关心这种区别(或者也许是call_user_func_array()......两种方式),但是看起来我们“真正”需要确保的是我们的call_user_func_array()调用中$ this-> bindArgs的每个成员的引用计数至少为 2 (请参阅帖子/问题的开头)。最简单的方法(在这种情况下)是使setArray()传递引用。


编辑:

为了获得额外的乐趣和游戏,我修改了原始程序(此处未显示)以使其等效于setArray()传值,并创建一个无偿的额外数组bindArgsCopy,其中包含与bindArgs完全相同的内容。这意味着,是的,两个数组都包含对“临时”数据的引用,这些数据在第二次调用时被解除分配。正如上面的分析所预测的,这是有效的。这表明上面的分析并不是var_dump()内部工作的神器(至少对我来说是一种解脱),它也证明了重要的引用计数,而不是原始的“临时性”。数据存储。

因此。我做了以下断言:在PHP中,出于call_user_func_array()(可能更多)的目的,说数据项是“引用”与说项目的引用计数大于或等于2是一回事(忽略PHP对等值标量的内部内存优化)


Administrivia注意:我很乐意给马里奥提供答案,因为他是第一个提出正确答案的人,但是因为他在评论中写了这个,而不是一个真正的答案,我做不到它: - (

3 个答案:

答案 0 :(得分:5)

将数组作为参考传递:

  function setArray(&$vals) {
    foreach ($vals as $key => &$value) {
      $this->myarray[] =& $value;
    }
    $this->dumpArray();
  }

我的猜测(在某些细节上可能是错误的,但希望在大多数情况下是正确的)至于为什么这会使你的代码按预期工作,当你作为一个值传递时,对{{{{{{{ 1}} dumpArray()内部,因为对setArray()内的$vals数组的引用仍然存在。但是当控制返回到setArray()时,那个临时变量就会消失,就像它对它的所有引用一样。因此,在为内存释放内存之前,PHP会尽可能地将数组更改为字符串值而不是引用。但如果您将其作为myfunc()的引用传递,则myfunc()使用对控件返回setArray()时生效的数组的引用。

答案 1 :(得分:2)

在参数签名中添加&为我修复了它。这意味着该函数将接收原始数组的内存地址。

function setArray(&$vals) {
// ...
}

CodePad

答案 2 :(得分:0)

我刚刚在几乎完全相同的情况下遇到了同样的问题(我为自己的目的编写了一个PDO包装器)。我怀疑PHP在没有其他变量引用相同数据的情况下将引用更改为值,而Rick,您对问题的编辑中的注释证实了这种怀疑,所以非常感谢您。

现在,对于遇到类似问题的其他人,我相信我有最简单的解决方案:在将数组传递给call_user_func_array()之前,只需将每个相关的数组元素设置为对自身的引用。

我不确定内部发生了什么,因为看起来它不应该工作,但该元素再次成为引用(你可以用var_dump()看到),然后call_user_func_array()通过按预期参考的论点。即使数组元素仍然是引用,此修复似乎也有效,因此您不必先检查。

在Rick的代码中,它会是这样的(在bind_param的第一个参数之后的所有内容都是通过引用,所以我跳过第一个并修复之后的所有代码):

for ($i = 1, $count = count($this->bindArgs); $i < $count; $i++) {
    $this->bindArgs[$i] = &$this->bindArgs[$i];
}

call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);