由于得分最高的答案表明第二种方法在各方面都更好,因此我觉得有必要在这里发布答案。正确,按引用循环is性能更高,但并非没有风险/陷阱。
底线,一如既往:“X 和 Y 哪个更好”,你能得到的唯一真正的答案是:
- 这取决于你在追求什么/你在做什么
- 哦,两者都可以,如果你知道自己在做什么
- X 适合Such, Y 更适合So
- 别忘了Z,即便如此......(“X、Y 或 Z 哪个更好”是同一个问题,所以适用相同的答案:这取决于,如果......)
尽管如此,正如 Orangepill 所表明的那样,参考方法提供了更好的性能。在这种情况下,性能与不易出错、更易于阅读/维护的代码之间的权衡之一。一般来说,最好选择更安全、更可靠、更可维护的代码:
“调试的难度是最初编写代码的两倍。因此,如果您尽可能巧妙地编写代码,那么根据定义,您就不够聪明,无法调试它。 — 布莱恩·科尼汉
我想这意味着必须考虑第一种方法最佳实践。但这并不意味着应该始终避免第二种方法,因此下面是在引用中使用引用时必须考虑的缺点、陷阱和怪癖。foreach
loop:
Scope:
首先,PHP 并不像 C(++)、C#、Java、Perl 或(运气好的话)ECMAScript6 那样真正具有块作用域...这意味着$value
多变的不会被取消设置一旦循环完成。当按引用循环时,这意味着对您正在迭代的任何对象/数组的最后一个值的引用正在浮动。词组“一场等待发生的事故”应该会浮现在脑海中。
考虑会发生什么$value
,随后$array
,在下面的代码中:
$array = range(1,10);
foreach($array as &$value)
{
$value++;
}
echo json_encode($array);
$value++;
echo json_encode($array);
$value = 'Some random value';
echo json_encode($array);
该片段的输出将是:
[2,3,4,5,6,7,8,9,10,11]
[2,3,4,5,6,7,8,9,10,12]
[2,3,4,5,6,7,8,9,10,"Some random value"]
换句话说,通过重用$value
变量(引用数组中的最后一个元素),您实际上是在操作数组本身。这使得代码容易出错并且调试困难。相对于:
$array = range(1,10);
$array[] = 'foobar';
foreach($array as $k => $v)
{
$array[$k]++;//increments foobar, to foobas!
if ($array[$k] === ($v +1))//$v + 1 yields 1 if $v === 'foobar'
{//so 'foobas' === 1 => false
$array[$k] = $v;//restore initial value: foobar
}
}
可维护性/防白痴:
当然,您可能会说悬空引用是一个简单的修复方法,您是对的:
foreach($array as &$value)
{
$value++;
}
unset($value);
但是,在您使用引用编写了前 100 个循环之后,您真的相信您不会忘记取消设置单个引用吗?当然不是!这是如此罕见unset
已在循环中使用的变量(我们假设 GC 会为我们处理它),所以大多数时候,您不必打扰。当涉及到引用时,这会是令人沮丧、神秘的错误报告或旅行价值,您使用复杂的嵌套循环,可能有多个引用......恐怖,恐怖。
此外,随着时间的推移,谁敢说下一个处理你的代码的人不会忘记unset
?谁知道呢,他可能甚至不知道参考资料,或者看到你的无数unset
打电话并认为它们是多余的,这是你偏执的表现,然后将它们全部删除。单独的注释对你没有帮助:它们需要被阅读,并且每个使用你的代码的人都应该被彻底介绍,也许有它们阅读有关该主题的完整文章 http://schlueters.de/blog/archives/141-References-and-foreach.html。链接文章中列出的示例很糟糕,但我还见过更糟糕的:
foreach($nestedArr as &$array)
{
if (count($array)%2 === 0)
{
foreach($array as &$value)
{//pointless, but you get the idea...
$value = array($value, 'Part of even-length array');
}
//$value now references the last index of $array
}
else
{
$value = array_pop($array);//assigns new value to var that might be a reference!
$value = is_numeric($value) ? $value/2 : null;
array_push($array, $value);//congrats, X-references ==> traveling value!
}
}
这是旅行价值问题的一个简单示例。顺便说一句,这不是我编造的,我遇到过可以归结为这一点的代码......老实说。除了发现错误和理解代码(参考资料使这变得更加困难)之外,在这个例子中它仍然相当明显,主要是因为它只有 15 行长,即使使用宽敞的 Allman 编码风格......现在想象一下这个基本结构被用在实际的代码中does甚至稍微复杂一点、更有意义的东西。祝你调试顺利。
副作用:
人们常说函数不应该有副作用,因为副作用(正确地)被认为是代码气味。尽管foreach
是一种语言构造,而不是函数,在您的示例中,应该应用相同的思维方式。当使用太多引用时,你就太聪明了,不利于自己,并且可能会发现自己必须单步执行循环,只是为了知道什么变量在何时引用什么。
第一种方法没有这个问题:你有密钥,所以你知道你在数组中的位置。更重要的是,使用第一种方法,您可以对值执行任意次数的操作,而无需更改数组中的原始值(无副作用):
function recursiveFunc($n, $max = 10)
{
if (--$max)
{
return $n === 1 ? 10-$max : recursiveFunc($n%2 ? ($n*3)+1 : $n/2, $max);
}
return null;
}
$array = range(10,20);
foreach($array as $k => $v)
{
$v = recursiveFunc($v);//reassigning $v here
if ($v !== null)
{
$array[$k] = $v;//only now, will the actual array change
}
}
echo json_encode($array);
这会生成输出:
[7,11,12,13,14,15,5,17,18,19,8]
正如您所看到的,第一、第七和第十元素已更改,其他元素没有更改。如果我们使用引用循环重写这段代码,循环看起来会小很多,但输出会有所不同(我们有一个副作用):
$array = range(10,20);
foreach($array as &$v)
{
$v = recursiveFunc($v);//Changes the original array...
//granted, if your version permits it, you'd probably do:
$v = recursiveFunc($v) ?: $v;
}
echo json_encode($array);
//[7,null,null,null,null,null,5,null,null,null,8]
为了解决这个问题,我们要么必须创建一个临时变量,要么调用函数tiwce,要么添加一个键,然后重新计算初始值$v
,但这简直是愚蠢的(这增加了修复不应该被破坏的东西的复杂性):
foreach($array as &$v)
{
$temp = recursiveFunc($v);//creating copy here, anyway
$v = $temp ? $temp : $v;//assignment doesn't require the lookup, though
}
//or:
foreach($array as &$v)
{
$v = recursiveFunc($v) ? recursiveFunc($v) : $v;//2 calls === twice the overhead!
}
//or
$base = reset($array);//get the base value
foreach($array as $k => &$v)
{//silly combine both methods to fix what needn't be a problem to begin with
$v = recursiveFunc($v);
if ($v === 0)
{
$v = $base + $k;
}
}
不管怎样,添加分支、临时变量和你拥有的东西,反而违背了这一点。首先,它引入了额外的开销,这将削弱参考文献最初为您提供的性能优势。
如果您必须向循环添加逻辑,以修复不需要修复的内容,那么您应该退后一步,考虑一下您正在使用的工具。 9/10 次,你选择了错误的工具来完成这项工作。
至少对我来说,第一种方法的最后一个令人信服的论据很简单:可读性。参考运算符 (&
)如果您正在做一些快速修复或尝试添加功能,则很容易被忽视。您可能会在运行良好的代码中创建错误。更重要的是:因为它工作正常,您可能不会彻底测试现有功能because没有已知问题。
由于您忽视操作员而发现投入生产的错误可能听起来很愚蠢,但您不会是第一个遇到这种情况的人。
Note:
自 5.4 起,调用时通过引用传递已被删除。对可能发生变化的特性/功能感到厌倦。数组的标准迭代多年来没有改变。我想这就是你可以这么称呼的“经过验证的技术”。它按照其承诺进行操作,并且是更安全的做事方式。那么如果速度慢了怎么办?如果速度是一个问题,您可以优化代码,然后引入对循环的引用。
编写新代码时,请选择易于阅读、最安全的选项。优化可以(而且确实should)等到一切都经过尝试和测试。
一如既往:过早的优化是万恶之源. And 选择适合工作的工具,而不是因为它是新的、闪亮的.