PHP Foreach Pass por referência: duplicação de último elemento? (Erro?)

159

Eu apenas tive um comportamento muito estranho com um script php simples que estava escrevendo. Eu o reduzi ao mínimo necessário para recriar o bug:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

Isso gera:

Array
(
    [0] => foo
    [1] => bar
    [2] => baz
)
Array
(
    [0] => foo
    [1] => bar
    [2] => bar
)

Isso é um bug ou algum comportamento realmente estranho que deveria acontecer?

realeza
fonte
Faça por valor novamente, veja se muda a 3ª vez ...?
Shackrock
1
@ Shackrock, parece que não muda mais com a repetição de loops por valor.
regality
1
Curiosamente, se você alterar o segundo loop para usar algo diferente de $ item, ele funcionará conforme o esperado.
Steve Claridge
9
sempre desmarque o item no final do corpo do loop: foreach($x AS &$y){ ... unset($y); }- ele está no php.net (não sei onde) porque é um erro muito cometido.
Rudie
2
possível duplicado de PHP passagem por referência na foreach
Felix Kling

Respostas:

170

Após o primeiro loop foreach, $itemainda é uma referência a algum valor que também está sendo usado por $arr[2]. Portanto, cada chamada foreach no segundo loop, que não chama por referência, substitui esse valor e $arr[2], portanto , pelo novo valor.

Então, faça um loop 1, o valor e $arr[2]torne - se $arr[0], que é 'foo'.
Loop 2, o valor e $arr[2]tornar - se $arr[1], que é 'bar'.
Loop 3, o valor e $arr[2]se tornar$arr[2] , que é 'bar' (por causa do loop 2).

O valor 'baz' é realmente perdido na primeira chamada do segundo loop foreach.

Depurando a saída

Para cada iteração do loop, ecoaremos o valor $iteme imprimiremos recursivamente a matriz$arr .

Quando o primeiro loop é executado, vemos esta saída:

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

No final do loop, $itemainda está apontando para o mesmo local que $arr[2].

Quando o segundo loop é executado, vemos esta saída:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

Você notará como cada matriz de tempo coloca um novo valor $item, também é atualizado $arr[3]com o mesmo valor, pois ambos ainda apontam para o mesmo local. Quando o loop atingir o terceiro valor da matriz, ele conterá o valor barporque foi definido apenas pela iteração anterior desse loop.

Isso é um bug?

Não. Esse é o comportamento de um item referenciado, e não um bug. Seria semelhante a executar algo como:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

Um loop foreach não é de natureza especial em que pode ignorar itens referenciados. É simplesmente definir essa variável para o novo valor sempre que você faria fora de um loop.

animuson
fonte
4
Eu tenho uma ligeira correção pedante. $itemnão é uma referência a $arr[2], o valor contido por $arr[2]é uma referência ao valor referido por $item. Para ilustrar a diferença, você também pode desconfigurar $arr[2], e $itemnão seria afetado, e escrever para $itemnão afetá-la.
precisa
2
Esse comportamento é complexo de entender e pode levar a problemas. Eu mantenho isso como um dos meus favoritos para mostrar aos meus alunos por que eles devem evitar (o máximo que puderem) as coisas "por referência".
Olivier Pons
1
Por que $itemnão sai do escopo quando o loop foreach é encerrado? Isso parece um problema de fechamento?
jocull
6
@ jocull: No PHP, foreach, for, while, etc não criam seu próprio escopo.
animuson
1
@ jocull, PHP não tem (bloco) variáveis ​​locais. Um dos motivos que me incomoda.
Qtax 13/04
29

$itemé uma referência $arr[2]e está sendo substituída pelo segundo loop foreach, como apontou animuson.

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?
Michael Leaney
fonte
3

Embora isso possa não ser oficialmente um bug, na minha opinião é. Penso que o problema aqui é que temos a expectativa de $itemsair do escopo quando o loop for encerrado, como ocorreria em muitas outras linguagens de programação. No entanto, isso não parece ser o caso ...

Este código ...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

Dá a saída ...

one
two
three
three

Como outras pessoas já disseram, você está substituindo a variável referenciada no $arr[2]seu segundo loop, mas isso só está acontecendo porque $itemnunca saiu do escopo. O que vocês acham ... bug?

jocull
fonte
4
1) Não é um bug. Ele já foi mencionado no manual e descartado em vários relatórios de erros, como pretendido. 2) Realmente não responde à pergunta ...
BoltClock
Ele me chamou a atenção não por causa do problema do escopo, eu esperava que o item $ permanecesse em torno após a pesquisa inicial, mas não percebi que a pesquisa atualiza a variável em vez de substituí-la. por exemplo, o mesmo que executar unset ($ item) antes do segundo loop. Observe que o unset não limpa o valor (e, portanto, o último elemento na matriz), simplesmente remove a variável.
Programster
Infelizmente, o PHP não cria um novo escopo para loops ou {}blocos em geral. Isto é como a linguagem funciona
Fabian Schmengler
0

O comportamento correto do PHP deve ser um erro de AVISO na minha opinião. Se uma variável referenciada criada em um loop foreach for usada fora do loop, isso causará um aviso. Muito fácil se apaixonar por esse comportamento, muito difícil identificá-lo quando aconteceu. E nenhum desenvolvedor lerá a página de documentação do foreach, não é uma ajuda.

Você deve unset()consultar após o seu loop para evitar esse tipo de problema. unset () em uma referência apenas removerá a referência sem danificar os dados originais.

John
fonte
0

isso é porque você usa pela diretiva ref (&). O último valor será substituído pelo segundo loop e corromperá sua matriz. a solução mais simples é usar um nome diferente para o segundo loop:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
Amir
fonte