Como o PHP 'foreach' realmente funciona?

2018

Deixe-me prefixar isso dizendo que sei o que foreaché, faz e como usá-lo. Esta pergunta diz respeito a como ela funciona sob o capô, e eu não quero nenhuma resposta na linha de "é assim que você faz um loop com uma matriz foreach".


Por um longo tempo, presumi que foreachfuncionasse com o próprio array. Então eu encontrei muitas referências ao fato de que ele funciona com uma cópia da matriz e, desde então, assumi que este é o fim da história. Mas recentemente entrei em uma discussão sobre o assunto e, depois de um pouco de experimentação, descobri que isso não era de fato 100% verdade.

Deixe-me mostrar o que quero dizer. Para os seguintes casos de teste, trabalharemos com a seguinte matriz:

$array = array(1, 2, 3, 4, 5);

Caso de teste 1 :

foreach ($array as $item) {
  echo "$item\n";
  $array[] = $item;
}
print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

Isso mostra claramente que não estamos trabalhando diretamente com a matriz de origem - caso contrário, o loop continuaria para sempre, pois estamos constantemente empurrando itens para a matriz durante o loop. Mas apenas para ter certeza de que este é o caso:

Caso de teste 2 :

foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

Isso confirma nossa conclusão inicial, estamos trabalhando com uma cópia da matriz de origem durante o loop, caso contrário veríamos os valores modificados durante o loop. Mas...

Se olharmos no manual , encontramos esta declaração:

Quando o foreach começa a executar pela primeira vez, o ponteiro interno da matriz é redefinido automaticamente para o primeiro elemento da matriz.

Certo ... isso parece sugerir que foreachdepende do ponteiro da matriz de origem. Mas acabamos de provar que não estamos trabalhando com a matriz de origem , certo? Bem, não inteiramente.

Caso de teste 3 :

// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/

Portanto, apesar de não estarmos trabalhando diretamente com a matriz de origem, estamos trabalhando diretamente com o ponteiro da matriz de origem - o fato de o ponteiro estar no final da matriz no final do loop mostra isso. Exceto que isso não pode ser verdade - se fosse, o caso de teste 1 seria repetido para sempre.

O manual do PHP também declara:

Como o foreach depende do ponteiro interno da matriz, alterá-lo no loop pode levar a um comportamento inesperado.

Bem, vamos descobrir o que é esse "comportamento inesperado" (tecnicamente, qualquer comportamento é inesperado, já que eu não sei mais o que esperar).

Caso de teste 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Caso de teste 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... nada de inesperado lá, na verdade, parece apoiar a teoria da "cópia da fonte".


A questão

O que está acontecendo aqui? Meu C-fu não é bom o suficiente para que eu possa extrair uma conclusão adequada simplesmente olhando o código-fonte PHP; eu apreciaria se alguém pudesse traduzi-lo para o inglês para mim.

Parece-me que foreachfunciona com uma cópia da matriz, mas define o ponteiro da matriz de origem para o final da matriz após o loop.

  • Isso está correto e a história toda?
  • Se não, o que está realmente fazendo?
  • Existe alguma situação em que o uso de funções que ajustam o ponteiro da matriz ( each(), reset()et al.) Durante a foreachpossa afetar o resultado do loop?
DaveRandom
fonte
5
@DaveRandom Há uma tag php-internals que provavelmente deve ser usada , mas deixarei para você decidir qual, se alguma das outras 5 tags, deve ser substituída.
Michael Berkowski
5
parece COW, sem excluir alça
zb '
149
No começo, pensei »nossa, outra pergunta para iniciantes. Leia os documentos… hm, comportamento claramente indefinido «. Depois li a pergunta completa e devo dizer: eu gosto. Você se esforçou bastante e escreveu todos os casos de teste. ps. os testcase 4 e 5 são iguais?
knittl
21
Apenas um pensamento sobre por que faz sentido que o ponteiro da matriz seja tocado: o PHP precisa redefinir e mover o ponteiro interno da matriz original junto com a cópia, porque o usuário pode solicitar uma referência ao valor atual ( foreach ($array as &$value)) - O PHP precisa conhecer a posição atual na matriz original, mesmo que esteja realmente repetindo uma cópia.
7603 Niko
4
@Sean: IMHO, a documentação do PHP é realmente muito ruim para descrever as nuances dos principais recursos da linguagem. Mas isso é, talvez, porque tantos casos ad hoc especial são assados para a língua ...
Oliver Charlesworth

Respostas:

1660

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 Traversableobjetos, pois, foreachessencialmente, 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 Iteratorinterface 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 $arrserá 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 foreachfaça uso do IAP, há uma complicação adicional: existe apenas um IAP, mas uma matriz pode fazer parte de vários foreachloops:

// 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, foreachexecute as seguintes travessuras: Antes de o corpo do loop ser executado, foreachfaç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 currentfamí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, foreachem muitos casos, é forçado a duplicar a matriz pela qual está iterando. As condições precisas são:

  1. 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.
  2. A matriz possui refcount> 1. Se refcountfor 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 refcountserá incrementada (*). Além disso, se foreachpor 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, $arrserá duplicado para impedir que as alterações do IAP $arrvazem 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 refcountaqui 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 refcounte, 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 unseto 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 foreachimplementaçã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, $arraycomeç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, foreachserá modificado o IAP da $arrayvariável. No final da iteração, o IAP é NULL (o que significa que a iteração foi concluída), o que eachindica retornando false.

  • Em casos de ensaio 4 e 5 ambos eache resetsão funções por referência. O $arraytem um refcount=2quando é passado para eles, portanto, ele deve ser duplicado. Como tal foreach, estará trabalhando em uma matriz separada novamente.

Exemplos: efeitos de currentno foreach

Uma boa maneira de mostrar os vários comportamentos de duplicação é observar o comportamento da current()função dentro de um foreachloop. 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 nextque são todas por referência. A passagem por referência implica que o array deve ser separado e, portanto, $arraye o foreach-arrayserá diferente. A razão que você obter 2em vez de 1também é mencionado acima: foreachavanç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, foreachjá 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 foreachde trabalho na mesma matriz. Você ainda vê o comportamento de um por um, devido à maneira como foreacho 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 $arraycria 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, $arraya 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 1foi 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 HashPointermecanismo 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 HashPointerrestauraçã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 foreachacreditar 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, 4acordo 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 foreachjá não usam o IAP em tudo . O foreachloop 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 refcountincremento (em vez de duplicar a matriz) em todos os casos. Se a matriz for modificada durante o foreachloop, nesse ponto ocorrerá uma duplicação (de acordo com a cópia na gravação) e foreachcontinuará 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 refcountingcomportamento par e duplicação é exatamente o mesmo entre o PHP 5 e o PHP 7).

  • O caso de teste 3 é alterado: Foreachnã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 foreachainda 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/refcountingconfiguraçõ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.

NikiC
fonte
4
@Baba Sim. Passando-a para uma função é o mesmo que fazer $foo = $arrayantes do laço;)
Nikić
32
Para aqueles que não sabem o que é um zval, consulte o blog de
shu zOMG chen
1
Correção menor: o que você chama de Bucket não é o que normalmente é chamado de Bucket em uma hashtable. Normalmente o intervalo é um conjunto de entradas com o mesmo tamanho de% de hash. Você parece usá-lo para o que normalmente é chamado de entrada. A lista vinculada não está nos buckets, mas nas entradas.
unbeli
12
@unbeli Estou usando a terminologia usada internamente pelo PHP. Os Buckets são parte de uma lista duplamente ligado para colisões de hash e também parte de uma lista duplamente ligada por ordem;)
Nikić
4
Grande resposta. Eu acho que você quis dizer iterate($outerArr);e não em iterate($arr);algum lugar.
Niahoo 31/03/16
116

No exemplo 3, você não modifica a matriz. Em todos os outros exemplos, você modifica o conteúdo ou o ponteiro interno da matriz. Isso é importante quando se trata de matrizes PHP devido à semântica do operador de atribuição.

O operador de atribuição para as matrizes no PHP funciona mais como um clone lento. Atribuir uma variável a outra que contém uma matriz clonará a matriz, diferente da maioria dos idiomas. No entanto, a clonagem real não será feita, a menos que seja necessário. Isso significa que o clone ocorrerá somente quando qualquer uma das variáveis ​​for modificada (copiar na gravação).

Aqui está um exemplo:

$a = array(1,2,3);
$b = $a;  // This is lazy cloning of $a. For the time
          // being $a and $b point to the same internal
          // data structure.

$a[] = 3; // Here $a changes, which triggers the actual
          // cloning. From now on, $a and $b are two
          // different data structures. The same would
          // happen if there were a change in $b.

Voltando aos seus casos de teste, você pode facilmente imaginar que foreachcria algum tipo de iterador com uma referência à matriz. Esta referência funciona exatamente como a variável $bno meu exemplo. No entanto, o iterador e a referência permanecem ativos apenas durante o loop e, em seguida, ambos são descartados. Agora você pode ver que, em todos os casos, exceto 3, a matriz é modificada durante o loop, enquanto essa referência extra está ativa. Isso aciona um clone, e isso explica o que está acontecendo aqui!

Aqui está um excelente artigo para outro efeito colateral desse comportamento de copiar na gravação: O operador ternário do PHP: Rápido ou não?

linepogl
fonte
Parece que você está certo, fiz um exemplo que demonstra que: codepad.org/OCjtvu8r uma diferença do seu exemplo - ele não copia se você alterar o valor, apenas se alterar as chaves.
Por ex'
Isso realmente explica todo o comportamento mostrado acima e pode ser bem ilustrado chamando each()no final do primeiro caso de teste, onde vemos que o ponteiro da matriz original aponta para o segundo elemento, uma vez que a matriz foi modificada durante a primeira iteração. Isso também parece demonstrar que foreachmove o ponteiro da matriz antes de executar o bloco de código do loop, o que eu não esperava - eu pensaria que isso seria feito no final. Muito obrigado, isso esclarece muito bem para mim.
precisa saber é o seguinte
49

Alguns pontos a serem observados ao trabalhar com foreach():

a) foreachtrabalha na cópia prospectada da matriz original. Isso significa foreach()que o armazenamento de dados será compartilhado até ou a menos que um prospected copynão seja criado para cada comentário do Notes / usuário .

b) O que desencadeia uma cópia prospectada ? Uma cópia prospectada é criada com base na política de copy-on-write, ou seja, sempre que uma matriz passada foreach()é alterada, um clone da matriz original é criado.

c) A matriz original e o foreach()iterador terão DISTINCT SENTINEL VARIABLES, ou seja, uma para a matriz original e outra para foreach; veja o código de teste abaixo. SPL , Iteradores e Array Iterator .

Pergunta sobre estouro de pilha Como garantir que o valor seja redefinido em um loop 'foreach' no PHP? aborda os casos (3,4,5) da sua pergunta.

O exemplo a seguir mostra que each () e reset () NÃO afetam SENTINELvariáveis (for example, the current index variable)do foreach()iterador.

$array = array(1, 2, 3, 4, 5);

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

foreach($array as $key => $val){
    echo "foreach: $key => $val<br/>";

    list($key2,$val2) = each($array);
    echo "each() Original(inside): $key2 => $val2<br/>";

    echo "--------Iteration--------<br/>";
    if ($key == 3){
        echo "Resetting original array pointer<br/>";
        reset($array);
    }
}

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

Resultado:

each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
sakhunzai
fonte
2
Sua resposta não está correta. foreachopera em uma cópia potencial da matriz, mas não faz a cópia real, a menos que seja necessário.
Linepogl # 8/12
você gostaria de demonstrar como e quando essa cópia em potencial é criada por meio de código? Meu código demonstra que foreachestá copiando a matriz 100% do tempo. Estou ansioso para saber. Obrigado por você comenta
sakhunzai
Copiar uma matriz custa muito. Tente contar o tempo necessário para iterar uma matriz com 100000 elementos usando forou foreach. Você não verá nenhuma diferença significativa entre os dois, porque uma cópia real não ocorre.
Linepogl # 9/12
Então, eu assumiria que há uma SHARED data storagereserva até ou a menos copy-on-write, mas (do meu trecho de código) é evidente que sempre haverá DOIS conjuntos de SENTINEL variablesum para o original arraye para outro foreach. Graças Isso faz sentido
sakhunzai
1
sim que é "prospecção" copiar ou seja copy.Its "potenciais" não protegidas como você sugeriu
sakhunzai
33

NOTA PARA PHP 7

Para atualizar esta resposta, pois ela ganhou popularidade: Esta resposta não se aplica mais a partir do PHP 7. Conforme explicado em " Alterações incompatíveis com versões anteriores ", no PHP 7 foreach funciona na cópia da matriz, portanto, quaisquer alterações na própria matriz não são refletidos no loop foreach. Mais detalhes no link.

Explicação (citação de php.net ):

O primeiro formulário faz um loop na matriz fornecida por array_expression. Em cada iteração, o valor do elemento atual é atribuído ao valor $ e o ponteiro interno da matriz é avançado em um (portanto, na próxima iteração, você verá o próximo elemento).

Portanto, no seu primeiro exemplo, você tem apenas um elemento na matriz e, quando o ponteiro é movido, o próximo elemento não existe; portanto, depois de adicionar um novo elemento, cada um deles termina porque ele já "decidiu" que ele é o último elemento.

No seu segundo exemplo, você começa com dois elementos, e o loop foreach não está no último elemento; portanto, ele avalia a matriz na próxima iteração e, portanto, percebe que há um novo elemento na matriz.

Eu acredito que tudo isso é consequência de Em cada iteração, parte da explicação na documentação, o que provavelmente significa que foreachfaz toda a lógica antes de chamar o código {}.

Caso de teste

Se você executar isso:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        $array['baz']=3;
        echo $v." ";
    }
    print_r($array);
?>

Você obterá esta saída:

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

O que significa que ele aceitou a modificação e passou por ela porque foi modificada "a tempo". Mas se você fizer isso:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Você vai ter:

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

O que significa que a matriz foi modificada, mas como a modificamos quando o elemento foreachjá estava no último elemento da matriz, ela "decidiu" não fazer mais um loop e, mesmo que adicionássemos novo elemento, adicionamos "tarde demais" e não foi repetido.

Explicação detalhada pode ser lida em Como o PHP 'foreach' realmente funciona? o que explica os internos por trás desse comportamento.

dkasipovic
fonte
7
Bem, você leu o resto da resposta? Faz todo o sentido que o foreach decida se ele fará um loop outra vez antes mesmo de executar o código nele.
Dkasipovic 15/04
2
Não, a matriz é modificada, mas "tarde demais", pois o foreach já "pensa" que está no último elemento (que está no início da iteração) e não será mais repetido. Onde no segundo exemplo, ele não está no último elemento no início da iteração e é avaliado novamente no início da próxima iteração. Estou tentando preparar um caso de teste.
Dkasipovic 15/04
1
@AlmaDo Veja lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Ele sempre é definido como o próximo ponteiro quando itera. Portanto, quando atingir a última iteração, será marcado como concluído (via ponteiro NULL). Quando você adiciona uma chave na última iteração, o foreach não notará.
bwoebi
1
@DKasipovic no. Não há completa e clara explicação lá (pelo menos por agora - pode ser que eu esteja errado)
Alma Do
4
Na verdade, parece que o @AlmaDo tem uma falha no entendimento de sua própria lógica ... Sua resposta está boa.
bwoebi
15

Conforme a documentação fornecida pelo manual do PHP.

Em cada iteração, o valor do elemento atual é atribuído a $ v e o
ponteiro interno da matriz é avançado em um (portanto, na próxima iteração, você verá o próximo elemento).

Então, como no seu primeiro exemplo:

$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}

$arraytem apenas um único elemento; portanto, de acordo com a execução do foreach, 1 atribuo a $ve ele não tem nenhum outro elemento para mover o ponteiro

Mas no seu segundo exemplo:

$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}

$arraytem dois elementos, então agora $ array avalia os índices zero e move o ponteiro por um. Para a primeira iteração do loop, adicionada $array['baz']=3;como passagem por referência.

user3535130
fonte
13

Ótima pergunta, porque muitos desenvolvedores, mesmo os mais experientes, ficam confusos com a maneira como o PHP lida com matrizes em loops foreach. No loop foreach padrão, o PHP faz uma cópia da matriz usada no loop. A cópia é descartada imediatamente após o término do loop. Isso é transparente na operação de um loop foreach simples. Por exemplo:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    echo "{$item}\n";
}

Isso gera:

apple
banana
coconut

Portanto, a cópia é criada, mas o desenvolvedor não percebe, porque a matriz original não é referenciada no loop ou após a conclusão do loop. No entanto, quando você tenta modificar os itens em um loop, descobre que eles não são modificados quando você termina:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $item = strrev ($item);
}

print_r($set);

Isso gera:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
)

Quaisquer alterações no original não podem ser avisos, na verdade não há alterações no original, mesmo que você tenha atribuído claramente um valor a $ item. Isso ocorre porque você está operando no item $, como aparece na cópia do conjunto de $ que está sendo trabalhado. Você pode substituir isso pegando $ item por referência, da seguinte maneira:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $item = strrev($item);
}
print_r($set);

Isso gera:

Array
(
    [0] => elppa
    [1] => ananab
    [2] => tunococ
)

Portanto, é evidente e observável, quando $ item é operado por referência, as alterações feitas em $ item são feitas nos membros do conjunto $ original. Usar $ item por referência também impede que o PHP crie a cópia da matriz. Para testar isso, primeiro mostraremos um script rápido demonstrando a cópia:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Isso gera:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
    [3] => Apple
    [4] => Banana
    [5] => Coconut
)

Como é mostrado no exemplo, o PHP copiou o $ set e o usou para fazer um loop, mas quando $ set foi usado dentro do loop, o PHP adicionou as variáveis ​​à matriz original, não à matriz copiada. Basicamente, o PHP está usando apenas a matriz copiada para a execução do loop e a atribuição do item $. Por esse motivo, o loop acima é executado apenas três vezes e, a cada vez, acrescenta outro valor ao final do conjunto $ original, deixando o conjunto original com 6 elementos, mas nunca inserindo um loop infinito.

No entanto, e se tivéssemos usado $ item por referência, como mencionei antes? Um único caractere adicionado ao teste acima:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Resultados em um loop infinito. Observe que, na verdade, é um loop infinito, você terá que matar o script sozinho ou esperar que o sistema operacional fique sem memória. Eu adicionei a seguinte linha ao meu script para que o PHP fique sem memória muito rapidamente, sugiro que você faça o mesmo se estiver executando esses testes de loop infinito:

ini_set("memory_limit","1M");

Portanto, neste exemplo anterior, com o loop infinito, vemos a razão pela qual o PHP foi escrito para criar uma cópia da matriz a ser repetida. Quando uma cópia é criada e usada apenas pela estrutura da própria construção do loop, a matriz permanece estática durante toda a execução do loop, para que você nunca tenha problemas.

Hrvoje Antunović
fonte
7

O loop foreach do PHP pode ser usado com Indexed arrays, Associative arrayse Object public variables.

No loop foreach, a primeira coisa que o php faz é que ele cria uma cópia da matriz que deve ser iterada. O PHP itera sobre esse novo copyda matriz e não o original. Isso é demonstrado no exemplo abaixo:

<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
    $numbers[$index] = $number + 1; # this is making changes to the origial array
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).

Além disso, o php permite usar iterated values as a reference to the original array valuetambém. Isso é demonstrado abaixo:

<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value

Nota: Não permite original array indexesser usado como references.

Fonte: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

Pranav Rana
fonte
1
Object public variablesestá errado ou, na melhor das hipóteses, enganoso. Você não pode usar um objeto em uma matriz sem a interface correta (por exemplo, Traversible) e, quando o faz, foreach((array)$obj ...trabalha de fato com uma matriz simples, não mais com um objeto.
Christian