我的 PHP 引用数组“神奇地”变成了一个值数组……为啥?

Posted

技术标签:

【中文标题】我的 PHP 引用数组“神奇地”变成了一个值数组……为啥?【英文标题】:My PHP array of references is "magically" becoming an array of values... why?我的 PHP 引用数组“神奇地”变成了一个值数组……为什么? 【发布时间】:2012-01-10 07:15:01 【问题描述】:

我正在围绕 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 注意:我很乐意将答案归功于 mario,因为他是第一个提出正确答案的人,但由于他是在评论中写的,而不是实际答案,所以我做不到它:-(

【问题讨论】:

setArray() 方法不是也需要获取数组作为参考参数吗?否则,您只是在创建对临时数组的引用。 @mario:先生,您说的完全正确,因为这确实解决了我的问题。这引出了一个问题……为什么这能解决问题?我希望对临时数组的引用仍然是引用,只是对由于引用本身而仅保存在内存中的东西的引用。如果我能找到这样的文档,我想我应该阅读 PHP 内存管理。 是的,将 &$vals 放入 setArray 函数中 很清楚为什么会发生这种情况。原始数组值对于第一个函数是本地的。一旦该函数完成,它就会被删除,您在该数组中的引用也会被删除。 PHP 很好,因为它会为您转换它们。参数中的 & 表示您通过引用传递值,并且它们在整个程序中仍然存在。有趣的是,您发现了这一点,但很清楚为什么会发生。 【参考方案1】:

将数组作为引用传递:

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

我的猜测(在某些细节上可能是错误的,但希望在大多数情况下是正确的)为什么这会使您的代码按预期工作时,当您作为值传递时,调用 @987654322 一切都很酷@ 在setArray() 内部,因为对setArray() 内部的$vals 数组的引用仍然存在。但是当控制权返回到myfunc() 时,那个临时变量就和所有对它的引用一样消失了。因此,PHP 在为其释放内存之前,尽职尽责地将数组更改为字符串值而不是引用。但是如果你将它作为来自myfunc() 的引用传递,那么setArray() 正在使用对一个数组的引用,该数组在控制权返回myfunc() 时仍然存在。

【讨论】:

【参考方案2】:

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

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

CodePad.

【讨论】:

【参考方案3】:

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

现在,对于遇到类似情况的任何其他人,我相信我有最简单的解决方案:只需在将数组传递给 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);

【讨论】:

以上是关于我的 PHP 引用数组“神奇地”变成了一个值数组……为啥?的主要内容,如果未能解决你的问题,请参考以下文章

【PHP】值为1的string类型,转成int类型后值变成了0?

想要在 PHP 中传递值列表(如在 Perl 中),而不是引用数组

PHP数组参数按值,在通过引用循环数组项时被修改

php数组通过复制值或引用分配? [复制]

php foreach使用引用的陷阱

如何通过单击更改数组值?