由于得分最高的答案表明第二种方法在各方面都更好,我觉得有必要在这里发布答案。诚然,按引用循环的性能更高,但并非没有风险/陷阱。
底线,一如既往:“哪个更好 X 或 Y”,你能得到的唯一真正答案是:
- 这取决于你追求什么/你在做什么
- 哦,两者都可以,如果你知道你在做什么
- X 适合这样,Y 适合So
- 不要忘记 Z,即使那样......(“哪个更好 X、Y 或 Z”是同一个问题,所以同样的答案适用:这取决于,如果......都可以)
尽管如此,正如 Orangepill 所展示的那样,参考方法提供了更好的性能。在这种情况下,性能与不易出错、更易于阅读/维护的代码之间的权衡之一。一般来说,最好选择更安全、更可靠、更易于维护的代码:
'调试的难度是一开始编写代码的两倍。因此,如果你尽可能巧妙地编写代码,根据定义,你就不够聪明,无法调试它。— 布赖恩·克尼汉
我想这意味着必须将第一种方法视为最佳实践。foreach
但这并不意味着应始终避免使用第二种方法,因此以下是在循环中使用引用时必须考虑的缺点、陷阱和怪癖:
范围:
首先,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
打电话并认为它们是多余的,这是您偏执的标志,然后将它们全部删除。单独的注释对你没有帮助:它们需要被阅读,并且每个使用你的代码的人都应该得到详尽的介绍,也许让他们阅读关于这个主题的完整文章。链接文章中列出的示例很糟糕,但我看到的情况更糟,仍然:
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 编码风格......现在想象一下这个基本结构被用在代码中,它实际上做了一些更复杂、更有意义的事情。祝你调试好运。
副作用:
人们常说函数不应该有副作用,因为副作用(理所当然地)被认为是代码异味。尽管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 次,您为工作选择了错误的工具。
最后一点,至少对我来说,是第一种方法的一个令人信服的论点很简单:可读性。&
如果您正在做一些快速修复或尝试添加功能,引用运算符 ( ) 很容易被忽略。您可能会在运行良好的代码中创建错误。更重要的是:因为它运行良好,您可能无法彻底测试现有功能,因为没有已知问题。
由于您忽略了操作员而发现投入生产的错误可能听起来很愚蠢,但您不会是第一个遇到这种情况的人。
注意:
自 5.4 以来,调用时通过引用传递已被删除。对可能发生变化的特性/功能感到厌烦。数组的标准迭代多年来没有改变。我想这就是你所说的“经过验证的技术”。它按照锡上所说的做,是更安全的做事方式。那么,如果它变慢了怎么办?如果速度是一个问题,您可以优化您的代码,然后引入对您的循环的引用。
编写新代码时,请选择易于阅读、最安全的选项。优化可以(并且确实应该)等到一切都经过试验和测试。
和往常一样:过早的优化是万恶之源。并为工作选择正确的工具,而不是因为它是新的和闪亮的。