foreach
suporta iteração em três tipos diferentes de valores:
A seguir, tentarei explicar com precisão como a iteração funciona em diferentes casos. De longe, o caso mais simples são os Traversable
objetos, pois, foreach
essencialmente, esses são apenas açúcar de sintaxe para o código, ao longo destas linhas:
foreach ($it as $k => $v) { /* ... */ }
/* translates to: */
if ($it instanceof IteratorAggregate) {
$it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
$v = $it->current();
$k = $it->key();
/* ... */
}
Para classes internas, as chamadas de método reais são evitadas usando uma API interna que basicamente apenas reflete a Iterator
interface no nível C.
A iteração de matrizes e objetos simples é significativamente mais complicada. Antes de tudo, deve-se notar que, no PHP, "matrizes" são realmente dicionários ordenados e serão percorridos de acordo com essa ordem (que corresponde à ordem de inserção, desde que você não tenha usado algo assim sort
). Isso se opõe à iteração pela ordem natural das chaves (como as listas em outros idiomas geralmente funcionam) ou por não ter nenhuma ordem definida (como os dicionários em outros idiomas costumam funcionar).
O mesmo se aplica aos objetos, pois as propriedades do objeto podem ser vistas como outro dicionário (ordenado) de mapeamento de nomes de propriedades para seus valores, além de alguma manipulação de visibilidade. Na maioria dos casos, as propriedades do objeto não são realmente armazenadas dessa maneira bastante ineficiente. No entanto, se você começar a iterar sobre um objeto, a representação compactada normalmente usada será convertida em um dicionário real. Nesse ponto, a iteração de objetos simples se torna muito semelhante à iteração de matrizes (é por isso que não estou discutindo muito a iteração de objeto simples aqui).
Por enquanto, tudo bem. Iterar sobre um dicionário não pode ser muito difícil, certo? Os problemas começam quando você percebe que uma matriz / objeto pode mudar durante a iteração. Existem várias maneiras de isso acontecer:
- Se você iterar por referência usando,
foreach ($arr as &$v)
então $arr
será transformado em uma referência e você poderá alterá-lo durante a iteração.
- No PHP 5, o mesmo se aplica mesmo se você iterar por valor, mas a matriz era uma referência anterior:
$ref =& $arr; foreach ($ref as $v)
- Os objetos têm semântica de passagem manipulada, o que, para propósitos mais práticos, significa que eles se comportam como referências. Portanto, os objetos sempre podem ser alterados durante a iteração.
O problema de permitir modificações durante a iteração é o caso em que o elemento em que você está atualmente é removido. Digamos que você use um ponteiro para acompanhar em qual elemento da matriz você está atualmente. Se esse elemento agora for liberado, você ficará com um ponteiro pendente (geralmente resultando em um segfault).
Existem diferentes maneiras de resolver esse problema. O PHP 5 e o PHP 7 diferem significativamente nesse aspecto e descreverei os dois comportamentos a seguir. O resumo é que a abordagem do PHP 5 foi bastante tola e levou a todos os tipos de problemas estranhos, enquanto a abordagem mais envolvida do PHP 7 resulta em um comportamento mais previsível e consistente.
Como última preliminar, deve-se notar que o PHP usa contagem de referência e cópia na gravação para gerenciar a memória. Isso significa que, se você "copiar" um valor, na verdade você apenas reutiliza o valor antigo e aumenta sua contagem de referência (refcount). Somente quando você realizar algum tipo de modificação, uma cópia real (chamada de "duplicação") será feita. Consulte Você está mentindo para obter uma introdução mais extensa sobre este tópico.
PHP 5
Ponteiro de matriz interno e HashPointer
As matrizes no PHP 5 têm um "ponteiro interno de matriz" (IAP) dedicado, que suporta corretamente modificações: Sempre que um elemento for removido, será verificado se o IAP aponta para esse elemento. Se isso acontecer, ele será avançado para o próximo elemento.
Embora foreach
faça uso do IAP, há uma complicação adicional: existe apenas um IAP, mas uma matriz pode fazer parte de vários foreach
loops:
// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
foreach ($arr as &$v) {
// ...
}
}
Para oferecer suporte a dois loops simultâneos com apenas um ponteiro interno de matriz, foreach
execute as seguintes travessuras: Antes de o corpo do loop ser executado, foreach
faça backup de um ponteiro para o elemento atual e seu hash em um for-foreach HashPointer
. Depois que o corpo do loop for executado, o IAP retornará a esse elemento se ele ainda existir. Se, no entanto, o elemento tiver sido removido, usaremos apenas onde quer que o IAP esteja atualmente. Esse esquema funciona quase que meio que tipo de trabalho, mas há um monte de comportamento estranho que você pode obter dele, alguns dos quais eu demonstrarei abaixo.
Duplicação de matriz
O IAP é um recurso visível de uma matriz (exposta através da current
família de funções), pois essas alterações no IAP contam como modificações na semântica de copiar na gravação. Infelizmente, isso significa que, foreach
em muitos casos, é forçado a duplicar a matriz pela qual está iterando. As condições precisas são:
- A matriz não é uma referência (is_ref = 0). Se é uma referência, então muda para isso são supostamente para propagar, por isso não deve ser duplicado.
- A matriz possui refcount> 1. Se
refcount
for 1, a matriz não será compartilhada e podemos modificá-la diretamente.
Se a matriz não for duplicada (is_ref = 0, refcount = 1), apenas sua refcount
será incrementada (*). Além disso, se foreach
por referência for usada, a matriz (potencialmente duplicada) será transformada em referência.
Considere este código como um exemplo em que ocorre duplicação:
function iterate($arr) {
foreach ($arr as $v) {}
}
$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);
Aqui, $arr
será duplicado para impedir que as alterações do IAP $arr
vazem para $outerArr
. Em termos das condições acima, a matriz não é uma referência (is_ref = 0) e é usada em dois locais (refcount = 2). Esse requisito é lamentável e um artefato da implementação abaixo do ideal (não há preocupação de modificação durante a iteração aqui, portanto, não precisamos realmente usar o IAP em primeiro lugar).
(*) Incrementar refcount
aqui parece inócuo, mas viola a semântica de cópia na gravação (COW): Isso significa que vamos modificar o IAP de uma matriz refcount = 2, enquanto a COW determina que as modificações só podem ser executadas em refcount = 1 valores. Essa violação resulta em alteração de comportamento visível ao usuário (enquanto uma COW é normalmente transparente) porque a alteração de IAP na matriz iterada será observável - mas apenas até a primeira modificação não-IAP na matriz. Em vez disso, as três opções "válidas" seriam: a) duplicar sempre, b) não incrementar o refcount
e, assim, permitir que a matriz iterada seja arbitrariamente modificada no loop ou c) não usar o IAP (o PHP 7 solução).
Ordem de avanço de posição
Há um último detalhe de implementação que você precisa conhecer para entender corretamente os exemplos de código abaixo. A maneira "normal" de percorrer alguma estrutura de dados seria algo parecido com isto no pseudocódigo:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
code();
move_forward(arr);
}
No entanto foreach
, sendo um floco de neve bastante especial, escolhe fazer as coisas de maneira ligeiramente diferente:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
move_forward(arr);
code();
}
Ou seja, o ponteiro da matriz já foi movido para frente antes da execução do corpo do loop. Isso significa que, enquanto o corpo do loop está trabalhando no elemento $i
, o IAP já está no elemento $i+1
. Essa é a razão pela qual as amostras de código que mostram modificações durante a iteração sempre serão unset
o próximo elemento, em vez do atual.
Exemplos: Seus casos de teste
Os três aspectos descritos acima devem fornecer uma impressão quase completa das idiossincrasias da foreach
implementação e podemos seguir em frente para discutir alguns exemplos.
O comportamento dos seus casos de teste é simples de explicar neste momento:
Nos casos de teste 1 e 2, $array
começa com refcount = 1, portanto não será duplicado por foreach
: Apenas o refcount
é incrementado. Quando o corpo do loop modifica subsequentemente a matriz (que possui refcount = 2 nesse ponto), a duplicação ocorrerá nesse ponto. O Foreach continuará trabalhando em uma cópia não modificada de $array
.
No caso de teste 3, mais uma vez a matriz não é duplicada, portanto, foreach
será modificado o IAP da $array
variável. No final da iteração, o IAP é NULL (o que significa que a iteração foi concluída), o que each
indica retornando false
.
Em casos de ensaio 4 e 5 ambos each
e reset
são funções por referência. O $array
tem um refcount=2
quando é passado para eles, portanto, ele deve ser duplicado. Como tal foreach
, estará trabalhando em uma matriz separada novamente.
Exemplos: efeitos de current
no foreach
Uma boa maneira de mostrar os vários comportamentos de duplicação é observar o comportamento da current()
função dentro de um foreach
loop. Considere este exemplo:
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
Aqui você deve saber que current()
é uma função by-ref (na verdade: prefer-ref), mesmo que não modifique a matriz. Tem que ser para ser agradável com todas as outras funções, como as next
que são todas por referência. A passagem por referência implica que o array deve ser separado e, portanto, $array
e o foreach-array
será diferente. A razão que você obter 2
em vez de 1
também é mencionado acima: foreach
avança o ponteiro do array antes de executar o código de utilizador, e não depois. Portanto, mesmo que o código esteja no primeiro elemento, foreach
já avançou o ponteiro para o segundo.
Agora vamos tentar uma pequena modificação:
$ref = &$array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
Aqui temos o caso is_ref = 1, para que o array não seja copiado (como acima). Mas agora que é uma referência, o array não precisa mais ser duplicado ao passar para a current()
função by-ref . Assim current()
e foreach
de trabalho na mesma matriz. Você ainda vê o comportamento de um por um, devido à maneira como foreach
o ponteiro avança.
Você obtém o mesmo comportamento ao fazer a iteração by-ref:
foreach ($array as &$val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
Aqui, a parte importante é que o foreach $array
cria um is_ref = 1 quando é iterado por referência, portanto, basicamente, você tem a mesma situação acima.
Outra pequena variação, desta vez, atribuiremos o array a outra variável:
$foo = $array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 1 1 1 1 1 */
Aqui, a refcount de $array
é 2 quando o loop é iniciado, portanto, pela primeira vez, precisamos fazer a duplicação antecipadamente. Assim, $array
a matriz usada pelo foreach será completamente separada do início. É por isso que você obtém a posição do IAP onde quer que estivesse antes do loop (nesse caso, estava na primeira posição).
Exemplos: modificação durante a iteração
Tentando explicar as modificações durante a iteração é onde todos os nossos problemas de foreach se originaram, por isso serve para considerar alguns exemplos para este caso.
Considere esses loops aninhados sobre a mesma matriz (onde a iteração by-ref é usada para garantir que realmente seja a mesma):
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Output: (1, 1) (1, 3) (1, 4) (1, 5)
A parte esperada aqui é que (1, 2)
está faltando na saída porque o elemento 1
foi removido. O que provavelmente é inesperado é que o loop externo para após o primeiro elemento. Por que é que?
A razão por trás disso é o hack do loop aninhado descrito acima: Antes de o corpo do loop ser executado, a posição atual do IAP e o hash são armazenados em backup em a HashPointer
. Após o corpo do loop, ele será restaurado, mas apenas se o elemento ainda existir, caso contrário, a posição atual do IAP (seja ela qual for) será usada. No exemplo acima, esse é exatamente o caso: O elemento atual do loop externo foi removido e, portanto, será utilizado o IAP, que já foi marcado como concluído pelo loop interno!
Outra consequência do HashPointer
mecanismo de backup + restauração é que as alterações no IAP através de reset()
etc. geralmente não causam impacto foreach
. Por exemplo, o código a seguir é executado como se reset()
não estivesse presente:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// output: 1, 2, 3, 4, 5
O motivo é que, embora reset()
modifique temporariamente o IAP, ele será restaurado no elemento foreach atual após o corpo do loop. Para forçar reset()
a afetar o loop, é necessário remover adicionalmente o elemento atual, para que o mecanismo de backup / restauração falhe:
$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
var_dump($value);
unset($array[1]);
reset($array);
}
// output: 1, 1, 3, 4, 5
Mas esses exemplos ainda são sensatos. A verdadeira diversão começa se você se lembrar que a HashPointer
restauração usa um ponteiro para o elemento e seu hash para determinar se ele ainda existe. Mas: os hashes têm colisões e os ponteiros podem ser reutilizados! Isso significa que, com uma escolha cuidadosa de chaves de matriz, podemos foreach
acreditar que um elemento que foi removido ainda existe, para que ele pule diretamente para ele. Um exemplo:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
reset($array);
var_dump($value);
}
// output: 1, 4
Aqui normalmente devemos esperar a saída de 1, 1, 3, 4
acordo com as regras anteriores. Como o que acontece é que 'FYFY'
tem o mesmo hash que o elemento removido 'EzFY'
e o alocador reutiliza o mesmo local de memória para armazenar o elemento. Assim, o foreach acaba pulando diretamente para o elemento recém-inserido, cortando assim o loop.
Substituindo a entidade iterada durante o loop
Um último caso estranho que eu gostaria de mencionar, é que o PHP permite que você substitua a entidade iterada durante o loop. Portanto, você pode começar a iterar em uma matriz e substituí-la por outra matriz no meio. Ou comece a iterar em uma matriz e substitua-a por um objeto:
$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];
$ref =& $arr;
foreach ($ref as $val) {
echo "$val\n";
if ($val == 3) {
$ref = $obj;
}
}
/* Output: 1 2 3 6 7 8 9 10 */
Como você pode ver neste caso, o PHP começará a iterar a outra entidade desde o início, depois que a substituição acontecer.
PHP 7
Iteradores hashtable
Se você ainda se lembra, o principal problema com a iteração de matriz era como lidar com a remoção de elementos no meio da iteração. O PHP 5 usou um único ponteiro interno de matriz (IAP) para esse fim, que foi um pouco abaixo do ideal, pois um ponteiro de matriz teve que ser esticado para suportar vários loops foreach simultâneos e interação com reset()
etc., além disso.
O PHP 7 usa uma abordagem diferente, a saber, ele suporta a criação de uma quantidade arbitrária de iteradores de hashtable externos seguros. Esses iteradores precisam ser registrados na matriz, a partir de então eles têm a mesma semântica que o IAP: Se um elemento da matriz for removido, todos os iteradores de hashtable que apontam para esse elemento serão avançados para o próximo elemento.
Isto significa que foreach
já não usam o IAP em tudo . O foreach
loop não terá absolutamente nenhum efeito nos resultados de current()
etc. e seu próprio comportamento nunca será influenciado por funções como reset()
etc.
Duplicação de matriz
Outra mudança importante entre o PHP 5 e o PHP 7 está relacionada à duplicação de matrizes. Agora que o IAP não é mais usado, a iteração por matriz de valor fará apenas um refcount
incremento (em vez de duplicar a matriz) em todos os casos. Se a matriz for modificada durante o foreach
loop, nesse ponto ocorrerá uma duplicação (de acordo com a cópia na gravação) e foreach
continuará trabalhando na matriz antiga.
Na maioria dos casos, essa alteração é transparente e não tem outro efeito senão melhor desempenho. No entanto, há uma ocasião em que resulta em comportamento diferente, a saber, o caso em que a matriz era uma referência anterior:
$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */
Anteriormente, a iteração por valor das matrizes de referência era casos especiais. Nesse caso, não ocorreu duplicação; portanto, todas as modificações da matriz durante a iteração seriam refletidas pelo loop. No PHP 7, este caso especial se foi: Uma iteração por valor de uma matriz sempre continuará trabalhando nos elementos originais, desconsiderando qualquer modificação durante o loop.
Obviamente, isso não se aplica à iteração por referência. Se você iterar por referência, todas as modificações serão refletidas pelo loop. Curiosamente, o mesmo se aplica à iteração por valor de objetos simples:
$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
var_dump($val);
$obj->bar = 42;
}
/* Old and new output: 1, 42 */
Isso reflete a semântica de manipulação de objetos (ou seja, eles se comportam como referência mesmo em contextos de valor).
Exemplos
Vamos considerar alguns exemplos, começando com seus casos de teste:
Os casos de teste 1 e 2 mantêm a mesma saída: a iteração da matriz por valor sempre continua trabalhando nos elementos originais. (Nesse caso, o refcounting
comportamento par e duplicação é exatamente o mesmo entre o PHP 5 e o PHP 7).
O caso de teste 3 é alterado: Foreach
não usa mais o IAP, portanto each()
não é afetado pelo loop. Ele terá a mesma saída antes e depois.
Os casos de teste 4 e 5 permanecem os mesmos: each()
e reset()
duplicam a matriz antes de alterar o IAP, enquanto foreach
ainda usam a matriz original. (Não que a alteração do IAP tenha importado, mesmo que a matriz tenha sido compartilhada.)
O segundo conjunto de exemplos estava relacionado ao comportamento de current()
diferentes reference/refcounting
configurações. Isso não faz mais sentido, pois não current()
é totalmente afetado pelo loop, portanto, seu valor de retorno sempre permanece o mesmo.
No entanto, obtemos algumas mudanças interessantes ao considerarmos modificações durante a iteração. Espero que você ache o novo comportamento mais saudável. O primeiro exemplo:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
// (3, 1) (3, 3) (3, 4) (3, 5)
// (4, 1) (4, 3) (4, 4) (4, 5)
// (5, 1) (5, 3) (5, 4) (5, 5)
Como você pode ver, o loop externo não é mais interrompido após a primeira iteração. O motivo é que os dois loops agora têm iteradores de hashtable totalmente separados e não há mais nenhuma contaminação cruzada de ambos os loops através de um IAP compartilhado.
Outro caso estranho de borda corrigido agora é o efeito estranho que você obtém ao remover e adicionar elementos que possuem o mesmo hash:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4
Anteriormente, o mecanismo de restauração do HashPointer pulava diretamente para o novo elemento porque "parecia" o mesmo que o elemento removido (devido à colisão de hash e ponteiro). Como não confiamos mais no hash do elemento para nada, isso não é mais um problema.