Aus*_*ust 16 php arrays foreach reference assign
请考虑以下PHP代码:
//Method 1
$array = array(1,2,3,4,5);
foreach($array as $i=>$number){
$number++;
$array[$i] = $number;
}
print_r($array);
//Method 2
$array = array(1,2,3,4,5);
foreach($array as &$number){
$number++;
}
print_r($array);
Run Code Online (Sandbox Code Playgroud)
两种方法都完成相同的任务,一种是通过分配引用而另一种是通过基于密钥重新分配来完成.我想在我的工作中使用优秀的编程技术,我想知道哪种方法是更好的编程实践?或者这是其中一个并不重要的事情?
Eli*_*gem 23
由于得分最高的答案表明第二种方法在各方面都更好,我觉得有必要在这里发表答案.诚然,通过引用循环是更好的性能,但它并非没有风险/陷阱.
底线,一如既往:"哪个更好的X或Y",你可以得到的唯一真正的答案是:
尽管如此,正如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);
Run Code Online (Sandbox Code Playgroud)
此代码段的输出将是:
[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"]
Run Code Online (Sandbox Code Playgroud)
换句话说,通过重用$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
}
}
Run Code Online (Sandbox Code Playgroud)
可维护性/傻逼性:
当然,您可能会说悬挂参考是一个简单的修复,你是对的:
foreach($array as &$value)
{
$value++;
}
unset($value);
Run Code Online (Sandbox Code Playgroud)
但是在你用引用编写了前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!
}
}
Run Code Online (Sandbox Code Playgroud)
这是旅行价值问题的一个简单例子.我没有提出这个问题,顺便说一下,我遇到的代码可以归结为...老实说.除了发现错误和理解代码(参考文献已经变得更加困难)之外,在这个例子中它仍然非常明显,主要是因为它只有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);
Run Code Online (Sandbox Code Playgroud)
这会生成输出:
[7,11,12,13,14,15,5,17,18,19,8]
Run Code Online (Sandbox Code Playgroud)
如你所见,第一,第七和第十个元素已被改变,其他元素则没有.如果我们使用循环引用重写此代码,则循环看起来要小得多,但输出会有所不同(我们有副作用):
$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]
Run Code Online (Sandbox Code Playgroud)
为了解决这个问题,我们要么必须创建一个临时变量,要么调用函数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;
}
}
Run Code Online (Sandbox Code Playgroud)
无论如何,添加分支,临时变量和你有什么,而不是失败.首先,它会引入额外的开销,这会消耗掉参考文献给你带来的性能优势.
如果你必须在循环中添加逻辑,修复一些不需要修复的东西,你应该退一步,考虑一下你正在使用的工具.9/10次,你为工作选择了错误的工具.
至少对我来说,最后一件事是第一种方法的一个令人信服的论点很简单:可读性.&如果您正在进行一些快速修复或尝试添加功能,则很容易忽略reference-operator().您可能会在代码中创建错误的错误.更重要的是:因为它工作正常,您可能无法彻底测试现有功能,因为没有已知问题.
发现一个生产中的错误,因为你忽略了操作员可能听起来很傻,但你不会是第一个遇到这个问题的人.
注意:
自5.4以来,已经删除了在呼叫时通过引用传递.厌倦了可能会发生变化的特性/功能.数组的标准迭代在几年内没有改变.我想这就是你所谓的"成熟技术".它完成了它在罐子上的说法,是更安全的做事方式.那么如果它更慢呢?如果速度是一个问题,您可以优化您的代码,然后引入您的循环的引用.
编写新代码时,请选择易于阅读,最安全的选项.优化可以(并且确实应该)等到所有尝试和测试.
和往常一样:过早优化是万恶之源.并为工作选择合适的工具,而不是因为它是新的和有光泽的.
就性能而言,方法2更好,特别是如果你有一个大数组和/或使用字符串键.
虽然两种方法都使用相同数量的内存,但第一种方法需要搜索数组,即使搜索是通过索引完成的,查找也会有一些开销.
鉴于此测试脚本:
$array = range(1, 1000000);
$start = microtime(true);
foreach($array as $k => $v){
$array[$k] = $v+1;
}
echo "Method 1: ".((microtime(true)-$start));
echo "\n";
$start = microtime(true);
foreach($array as $k => &$v){
$v+=1;
}
echo "Method 2: ".((microtime(true)-$start));
Run Code Online (Sandbox Code Playgroud)
平均产量是
Method 1: 0.72429609298706
Method 2: 0.22671484947205
Run Code Online (Sandbox Code Playgroud)
如果我将测试缩减到仅运行十次而不是一百万次,我得到的结果就像
Method 1: 3.504753112793E-5
Method 2: 1.2874603271484E-5
Run Code Online (Sandbox Code Playgroud)
使用字符串键,性能差异更加明显.好跑
$array = array();
for($x = 0; $x<1000000; $x++){
$array["num".$x] = $x+1;
}
$start = microtime(true);
foreach($array as $k => $v){
$array[$k] = $v+1;
}
echo "Method 1: ".((microtime(true)-$start));
echo "\n";
$start = microtime(true);
foreach($array as $k => &$v){
$v+=1;
}
echo "Method 2: ".((microtime(true)-$start));
Run Code Online (Sandbox Code Playgroud)
产量表现如
Method 1: 0.90371179580688
Method 2: 0.2799870967865
Run Code Online (Sandbox Code Playgroud)
这是因为按字符串键搜索比数组索引有更多的开销.
值得注意的是,正如Elias Van Ootegem的回答中提到的那样,在你自己完成清理之后,你应该在循环完成之后取消参考.即unset($v);,应该根据可读性的损失来衡量性能增益.