18

考虑以下 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);

两种方法完成相同的任务,一种通过分配引用,另一种通过基于键重新分配。我想在我的工作中使用好的编程技术,我想知道哪种方法是更好的编程实践?或者这是那些无关紧要的事情之一?

4

8 回答 8

24

由于得分最高的答案表明第二种方法在各方面都更好,我觉得有必要在这里发布答案。诚然,按引用循环的性能更高,但并非没有风险/陷阱。
底线,一如既往:“哪个更好 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 以来,调用时通过引用传递已被删除。对可能发生变化的特性/功能感到厌烦。数组的标准迭代多年来没有改变。我想这就是你所说的“经过验证的技术”。它按照锡上所说的做,是更安全的做事方式。那么,如果它变慢了怎么办?如果速度是一个问题,您可以优化您的代码,然后引入对您的循环的引用。
编写新代码时,请选择易于阅读、最安全的选项。优化可以(并且确实应该)等到一切都经过试验和测试。

和往常一样:过早的优化是万恶之源。并为工作选择正确的工具,而不是因为它是新的和闪亮的

于 2013-08-27T11:22:51.253 回答
6

就性能而言,方法 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));

平均输出为

Method 1: 0.72429609298706
Method 2: 0.22671484947205

如果我将测试缩减为只运行 10 次而不是 100 万次,我会得到类似的结果

Method 1: 3.504753112793E-5
Method 2: 1.2874603271484E-5

使用字符串键,性能差异更加明显。就这么跑。

$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));

产生类似的表现

Method 1: 0.90371179580688
Method 2: 0.2799870967865

这是因为按字符串键搜索比数组索引有更多开销。

还值得注意的是,正如Elias Van Ootegem 的回答中建议的那样,在您自己正确清理之后,您应该在循环完成后取消设置引用。即unset($v);,性能增益应该根据可读性的损失来衡量。

于 2013-08-26T13:31:56.823 回答
3

有一些细微的性能差异,但它们不会产生任何显着影响。

我会选择第一个选项有两个原因:

  1. 它更具可读性。这有点个人喜好,但乍一看,$number++更新数组对我来说并不是很明显。通过明确使用类似的东西$array[$i]++,它会更清晰,并且当您在一年后回到此代码时不太可能引起混淆。

  2. 它不会给您留下对数组中最后一项的悬空引用。考虑这段代码:

    $array = array(1,2,3,4,5);
    foreach($array as &$number){
        $number++;
    }
    
    // ... some time later in an unrelated section of code
    $number = intval("100");
    
    // now unexpectedly, $array[4] == 100 instead of 6
    
于 2013-07-03T23:41:29.190 回答
1

我想这取决于。您是否更关心代码的可读性/可维护性或最小化内存使用量。第二种方法会使用稍少的内存,但老实说,我更喜欢第一种用法,因为在 foreach 定义中通过引用分配似乎在 PHP 中并不常见。

就个人而言,如果我想像这样修改数组,我会选择第三种选择:

array_walk($array, function(&$value) {
    $value++;
});  
于 2013-07-03T23:15:46.027 回答
0

第一种方法会稍微慢一些,因为每次它通过循环时,它都会为 $number 变量分配一个新值。第二种方法直接使用变量,因此不需要为每个循环分配新值。

但是,正如我所说,差异并不显着,主要考虑的是可读性。

我认为,当您不需要修改循环中的值时,第一种方法更有意义,只会读取 $number 变量。

当您需要经常修改 $number 变量时,第二种方法更有意义,因为您不需要每次修改它时都重复键,并且它更具可读性。

于 2013-07-03T23:13:30.163 回答
0

你考虑过array_map吗?它旨在更改数组内的值。

$array = array(1,2,3,4,5);
$new = array_map(function($number){
  return $number++ ;
}, $array) ;
var_dump($new) ;
于 2013-07-03T23:15:39.827 回答
0

我会选择#2,但这是个人喜好。

我不同意其他答案,在 foreach 循环中使用对数组项的引用很常见,但这取决于您使用的框架。与往常一样,尝试遵循项目或框架中现有的编码约定。

我也不同意其他建议 array_map 或 array_walk 的答案。这些为每个数组元素引入了函数调用的开销。对于小型数组,这并不重要,但对于大型数组,这将为这样一个简单的函数增加大量开销。但是,如果您正在执行更重要的计算或操作,它们是合适的 - 您需要根据场景决定使用哪个,也许通过基准测试。

于 2013-07-03T23:23:04.983 回答
0

大多数答案都将您的问题解释为与性能有关。

这不是你问的。你问的是:

我想知道哪种方法是更好的编程实践?

正如你所说,两者都做同样的事情。两者都工作。最后,更好通常是见仁见智的问题。

或者这是那些无关紧要的事情之一?

我什至不会说没关系。如您所见,方法 1可能存在性能考虑因素,方法 2 可能存在参考陷阱

我可以说重要的是可读性和一致性。虽然在 PHP 中有许多方法可以增加数组元素,但有些方法看起来像线条噪音或代码高尔夫。

确保您的代码对未来的开发人员是可读的,并且您始终如一地应用解决问题的方法是一种比此代码中存在的任何微小差异更好的宏编程实践。foreach

于 2013-08-28T02:35:39.890 回答