由于最高得分的答案表明第二种方法在各方面都更好,因此我感到有必要在此发布一个答案。的确,通过引用循环更具性能,但并非没有风险和陷阱。
底线始终如一:
“哪个更好X或Y”,您可以得到唯一真正的答案是:
- 这取决于你想要什么/你正在做什么
- 如果您知道自己在做什么,则两种方法都可以
- X适用于这样的事情,Y则更适合那样的事情
- 不要忘记Z,即使这样...(“哪个更好X、Y还是Z”是相同的问题,因此相同的答案适用:这取决于,如果两者都可以...)
尽管如此,就像Orangepill展示的那样,引用方法提供了更好的性能。在这种情况下,权衡是性能与代码之间易于出错、易于阅读/维护。通常认为,更安全、更可靠、更易于维护的代码更好:
'调试比首次编写代码困难两倍。因此,如果您尽可能聪明地编写代码,则从定义上来说,您不够聪明来调试它。' —— Brian Kernighan
我想这意味着第一种方法必须被认为是最佳实践。但这并不意味着第二种方法应该始终避免,因此下面列出的是在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]++;
if ($array[$k] === ($v +1))
{
$array[$k] = $v;
}
}
可维护性/傻瓜式操作:
当然,你可能会说悬空引用是容易修复的问题,而且你是正确的:
foreach($array as &$value)
{
$value++;
}
unset($value);
但是,当你写了100个带有引用的循环后,你真的相信你没有忘记取消一个引用吗?当然不是!这么不常见的情况下,去 unset
在循环中使用过的变量(我们假设垃圾回收会为我们处理),所以大多数时候,你都不会费心。但是当涉及到引用时,这是一种令人沮丧的来源,神秘的错误报告或者 旅行值,在其中你使用复杂的嵌套循环,可能还带有多个引用……恐怖,恐怖。
此外,随着时间的推移,谁能保证下一个工作在你代码上的人不会忘记 unset
呢?谁知道他甚至可能不知道引用,或者看到你众多的 unset
调用,认为它们是多余的,是你过于紧张的标志,并将它们全部删除。仅靠注释是不够的:它们需要被阅读,每个与你的代码一起工作的人都应该接受彻底的指导,也许让他们阅读一篇关于此主题的完整文章。链接文章中列举的示例很糟糕,但我见过更糟糕的:
foreach($nestedArr as &$array)
{
if (count($array)%2 === 0)
{
foreach($array as &$value)
{
$value = array($value, 'Part of even-length array');
}
}
else
{
$value = array_pop($array);
$value = is_numeric($value) ? $value/2 : null;
array_push($array, $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]
为了解决这个问题,我们可以创建一个临时变量、调用函数两次、添加一个键并重新计算
$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的情况下,你选择了错误的工具解决问题。
对我而言,最令人信服的第一种方法是简单易读:引用运算符(&)容易被忽视,特别是当你进行一些快速修补或尝试添加功能时。你可能会在原本正常运行的代码中引入bug。更重要的是,因为它本来就很好用,你可能不会仔细测试现有功能,因此忽略操作符而导致生产环境中出现bug听起来可能很傻,但你不会是第一个遇到这个问题的人。
注意:自5.4以来,在调用时通过引用传递已被删除。请小心使用可能会发生变化的功能。数组的标准迭代已经多年没有改变。我想这就是所谓的“成熟技术”。它做到了承诺,并且是更安全的做法。即使它速度较慢,那又怎样?如果速度是一个问题,你可以优化你的代码,并在循环中使用引用。编写新代码时,请选择易于阅读和最安全的选项。优化可以(并且确实应该)等到一切都经过尝试和测试之后再进行。
同时:过早地优化是万恶之源。选择正确的工具,不要只因为它是新的或闪亮的。
foreach
来递增一个数字?不,这不是foreach
的用途。该示例旨在向您展示可以通过引用使用值。可以认为array_map
、array_walk
等是更好的解决方案;但这实际上取决于上下文... - Rob W