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

Ric*_*shi 11 php arrays mysqli reference object

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

call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);
Run Code Online (Sandbox Code Playgroud)

我收到一条错误消息:

Parameter 2 to mysqli_stmt::bind_param() expected to be a reference, value given
Run Code Online (Sandbox Code Playgroud)

上面的讨论是为了那些会说"你的例子中根本不需要引用"的人.

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

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'));
Run Code Online (Sandbox Code Playgroud)

问题似乎是,在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"
}
Run Code Online (Sandbox Code Playgroud)

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

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


编辑:

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

function setArray(&$vals) {
Run Code Online (Sandbox Code Playgroud)

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

PHP,特别是mysqli,似乎对"引用"的概念略显奇怪.观察这个例子:

$a = "foo";
$b = array(&$a);
$c = array(&$a);
var_dump($b);
var_dump($c);
Run Code Online (Sandbox Code Playgroud)

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

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

unset($a);
var_dump($b);
var_dump($c);
Run Code Online (Sandbox Code Playgroud)

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

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

unset($b);
var_dump($c);
Run Code Online (Sandbox Code Playgroud)

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

PHP手册说,有一个独立的标志,对每一块数据的"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);
Run Code Online (Sandbox Code Playgroud)

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

超出我的原因,mysqli :: bind_param()首先关心这种区别(或者也许是call_user_func_array()......无论如何),但看起来我们"真正"需要确保的是参考对于call_user_func_array()调用中$ this-> bindArgs的每个成员,count至少为2(请参阅帖子/问题的开头).最简单的方法(在这种情况下)是使setArray()传递引用.


编辑:

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

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


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

Tro*_*ott 5

传递数组作为参考:

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

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