Foreach com referência em PHP

code, php, web

No PHP, as variáveis podem ser de 2 tipos: valores primitivos ou referências. Para especificar que uma variável será passada por referências em uma função, nós usamos o & na frente do nome da variável. Assim como em muitas linguagens, alterar uma variável passada por referência altera o valor da variável ao qual ela está se referindo. Caso não seja marcada com o & e a variável receba um valor primitivo, a variável representará um valor primitivo e qualquer alteração nessa variável não influencia outras variáveis. Diante disso, vamos analisar um exemplo. Qual a saída do seguinte código?

$list = [0, 0, 0];
foreach ($list as $key => &$field) {
  $field = $key + 1;
}
print_r($list);

foreach ($list as $key => $field) {
  $field = $field + 3;
}
print_r($list);

Alternativa A

Output:Array
(
    [0] => 0
    [1] => 0
    [2] => 0
)
Array
(
    [0] => 0
    [1] => 0
    [2] => 0
)
Alternativa B

Output:Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
Array
(
    [0] => 4
    [1] => 5
    [2] => 6
)
Alternativa C

Output:Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)

Imagem com códigos em PHP para não ter um espaço entre a pergunta e a resposta

Se você respondeu alternativa A, B ou C, você errou. A resposta correta é a seguinte:

Output:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
Array
(
    [0] => 1
    [1] => 2
    [2] => 8
)

Mas como é possível isso? Da onde surgiu esse 8? Somente depois de alguns bugs em produção que eu descobri o que esse código realmente faz.

Vamos analisar o primeiro foreach.

$list = [0, 0, 0];
foreach ($list as $key => &$field) {
  $field = $key + 1;
}
print_r($list);

Esse foreach utiliza a variável $field como referência. Por isso, na primeira iteração, $field aponta para o primeiro item do array. Como $key = 0, temos:

$list = [
    1,   // $field está apontando para este item
    0, 
    0
];

Na segunda iteração, $key = 1:

$list = [
    1,   
    2,  // $field está apontando para este item
    0
];

Na terceira iteração, $key = 2:

$list = [
    1,   
    2,  
    3   // $field está apontando para este item
];

Por isso, quando imprimos o valor de $list pela primeira vez, temos:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)

Nenhuma novidade. Exatamente como esperado. Como usamos a variável $field como referência, é natural que o array original seja alterado. Entretanto, vamos para o segundo foreach:

foreach ($list as $key => $field) {
  $field = $key + 3;
}
print_r($list);

Qual o valor inicial de $list?

$list = [
    1,   
    2,  
    3   // $field está apontando para este item
];

E esse $field? Aqui, temos uma observação importante: a variável $field ainda está apontando para o último item da lista. O que acontece na próxima instrução?

foreach ($list as $key => $field)

Quando o foreach é executado, note que estamos atribuíndo o primeiro item da lista para a variável $field. O que é a variável $field? O último elemento da lista. O primeiro item da lista é o 1. Por isso, estamos atribuíndo 1 para o último item da lista. Assim, no início da primeira iteração, temos:

$list = [
    1,   
    2,  
    1   // $field está apontando para este item
];

Após $field = $key + 3;, temos:

$list = [
    1,   
    2,  
    4   // $field está apontando para este item
];

No início da segunda iteração, estamos atribuíndo o valor do segundo item para a variável field. Por isso, o valor da lista no início da segunda iteração será:

$list = [
    1,   
    2,  
    2   // $field está apontando para este item
];

Por isso, no final da segunda iteração, nós temos:

$list = [
    1,   
    2,  
    5   // $field está apontando para este item
];

No início da terceira iteração, o valor do terceiro item é atribuído para $field. Como o valor já é 5, não há mudanças:

$list = [
    1,   
    2,  
    5   // $field está apontando para este item
];

No final da terceira iteração, é adicionado 3 na variável $field.

$list = [
    1,   
    2,  
    8   // $field está apontando para este item
];

Por fim, o último print exibe o seguinte resultado:

Array
(
    [0] => 1
    [1] => 2
    [2] => 8
)

Conclusão

Para programadores novos de PHP (ou mesmo para programadores com anos de experiência), esse comportamento é contraintuitivo e pode gerar bugs muito difíceis de rastrear. Logo, é necessário tomar certos cuidados quando for utilizado referências em PHP. Eu sugiro sempre que possível evitar o uso de variáveis de referências. Entretanto, se não for possível, lembre-se de limpar a variável de referência assim que não for mais necessária.

$list = [0, 0, 0];
foreach ($list as $key => &$field) {
  $field = $key + 1;
}
unset($field);
print_r($list);

foreach ($list as $key => $field) {
  $field = $field + 3;
}
print_r($list);

Como foi feito unset da variável $field, o resultado intuitivo é obtido:

Output:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)