我正在创建一个围绕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注意:我很乐意给马里奥提供答案,因为他是第一个提出正确答案的人,但是因为他在评论中写了这个,而不是一个真正的答案,我做不到它: - (
答案 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)
答案 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);