Jak działa PHP 'foreach'?

Pozwól mi to przedrostkiem mówiąc, że wiem, co foreach jest, robi i jak go używać. To pytanie dotyczy tego, jak to działa pod maską i nie chcę żadnych odpowiedzi w stylu " tak zapętla się tablicę z foreach".


Przez długi czas zakładałem, że foreach działa z samą tablicą. Potem znalazłem wiele odniesień do faktu, że działa on z kopią tablicy i od tego czasu założyłem, że to koniec historii. Ale Ja niedawno wdał się w dyskusję na ten temat, a po kilku eksperymentach okazało się, że nie było to w rzeczywistości 100% prawda.

Pozwól mi pokazać, co mam na myśli. Dla następujących przypadków testowych będziemy pracować z następującą tablicą:

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

Przypadek testowy 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 */

To wyraźnie pokazuje, że nie pracujemy bezpośrednio z tablicą źródłową - w przeciwnym razie pętla będzie kontynuowana w nieskończoność, ponieważ ciągle wciskamy elementy do tablicy podczas pętli. Ale dla pewności tak jest:

Przypadek testowy 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 */

To potwierdza nasz wstępny wniosek, pracujemy z kopią tablicy źródłowej podczas pętli, w przeciwnym razie zobaczylibyśmy zmodyfikowane wartości podczas pętli. Ale...

Jeśli spojrzymy w instrukcji , znajdziemy takie stwierdzenie:

Gdy foreach po raz pierwszy rozpocznie wykonywanie, wewnętrzny wskaźnik tablicy jest automatycznie resetowany do pierwszego elementu / align = "left" /

Racja... to wydaje się sugerować, że foreach opiera się na wskaźniku tablicy tablicy źródłowej. Ale właśnie dowiodliśmy, że nie pracujemy z tablicą źródłową, prawda? Nie do końca.

Przypadek testowy 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)
*/

Tak więc, pomimo faktu, że nie pracujemy bezpośrednio z tablicą źródłową, pracujemy bezpośrednio ze wskaźnikiem tablicy źródłowej - fakt, że wskaźnik znajduje się na końcu tablicy na końcu pętli pokazuje to. Tylko, że to nie może być prawda - gdyby tak było, to przypadek testowy 1 zapętliłby się w nieskończoność.

Podręcznik PHP stwierdza również:

Ponieważ foreach opiera się na wewnętrznym wskaźniku tablicy, Zmiana go w pętli może prowadzić do nieoczekiwanego zachowania.

Cóż, dowiedzmy się, co to jest " nieoczekiwane zachowanie "(technicznie rzecz biorąc, każde zachowanie jest nieoczekiwane, ponieważ Nie wiem już, czego się spodziewać).

Przypadek testowy 4:

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

/* Output: 1 2 3 4 5 */

Przypadek testowy 5:

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

/* Output: 1 2 3 4 5 */

...nic tak nieoczekiwanego, w rzeczywistości wydaje się popierać teorię "kopii źródła".


Pytanie

Co tu się dzieje? Mój C-fu nie jest wystarczająco dobry dla mnie, aby móc wyciągnąć właściwy wniosek po prostu patrząc na kod źródłowy PHP, byłbym wdzięczny, gdyby ktoś mógł przetłumaczyć go na angielski dla mnie.

Wydaje mi się, że foreach działa z kopią tablicy, ale ustawia wskaźnik tablicy tablicy źródłowej do końca tablicy za pętlą.

  • czy to prawda i cała historia?
  • jeśli nie, to co tak naprawdę robi?
  • czy jest jakaś sytuacja, w której używanie funkcji dostosowujących wskaźnik tablicy (each(), reset() i in.) podczas foreach może wpłynąć na wynik pętli?
Author: sergiol, 2012-04-07

7 answers

foreach obsługuje iterację trzech różnych typów wartości:

W dalszej części postaram się dokładnie wyjaśnić, jak iteracja działa w różnych przypadkach. Zdecydowanie najprostszym przypadkiem są Traversable obiekty, ponieważ dla tych foreach jest w zasadzie tylko cukrem składniowym dla kodu w tych liniach:

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();
    /* ... */
}

Dla klas wewnętrznych, rzeczywiste wywołania metod są unikane przez użycie wewnętrzne API, które zasadniczo odzwierciedla interfejs Iterator na poziomie C.

Iteracja tablic i zwykłych obiektów jest znacznie bardziej skomplikowana. Przede wszystkim należy zauważyć, że w PHP "tablice" są tak naprawdę uporządkowanymi słownikami i będą przesuwane zgodnie z tą kolejnością(która pasuje do kolejności wstawiania, o ile nie użyłeś czegoś takiego jak sort). Jest to sprzeczne z iteracją przez naturalną kolejność kluczy (jak często działają listy w innych językach) lub nie mając w ogóle określonej kolejności (jak często działają słowniki w innych językach).

To samo odnosi się również do obiektów, ponieważ właściwości obiektu mogą być postrzegane jako inny (uporządkowany) słownik mapujący nazwy właściwości do ich wartości, plus Obsługa widoczności. W większości przypadków właściwości obiektu nie są faktycznie przechowywane w ten raczej nieefektywny sposób. Jeśli jednak rozpoczniesz iterację nad obiektem, zwykle używana reprezentacja spakowana zostanie przekonwertowana na prawdziwy słownik. W tym momencie iteracja zwykłych obiektów staje się bardzo podobna do iteracji tablic (dlatego nie dyskutuję tu zbyt często o iteracji zwykłych obiektów).

Jak na razie dobrze. Tłumaczenie słownika nie może być zbyt trudne, prawda? Problemy zaczynają się, gdy zdajesz sobie sprawę, że tablica/obiekt może się zmieniać podczas iteracji. Istnieje wiele sposobów, aby to się stało:
  • jeśli iterujesz przez odniesienie używając foreach ($arr as &$v) wtedy {[25] } zostanie zamienione w odniesienie I ty może go zmienić podczas iteracji.
  • w PHP 5 to samo dotyczy nawet jeśli iterujesz według wartości, ale tablica była wcześniej referencją: $ref =& $arr; foreach ($ref as $v)
  • obiekty mają semantykę przekazującą by-handle, co dla większości praktycznych celów oznacza, że zachowują się jak odniesienia. Tak więc obiekty mogą być zawsze zmieniane podczas iteracji.

Problem z zezwoleniem na modyfikacje podczas iteracji polega na tym, że element, na którym aktualnie się znajdujesz, jest usuwany. Powiedz, że używasz wskaźnika aby śledzić, w którym elemencie tablicy aktualnie się znajdujesz. Jeśli ten element jest teraz zwolniony, zostanie Ci zwisający wskaźnik (Zwykle skutkujący segfault).

Istnieją różne sposoby rozwiązania tego problemu. PHP 5 i PHP 7 różnią się znacząco pod tym względem i opiszę oba zachowania w następujący sposób. Podsumowanie jest takie, że podejście PHP 5 było raczej głupie i prowadzi do wszelkiego rodzaju dziwnych problemów z krawędzią, podczas gdy bardziej zaangażowane podejście PHP 7 skutkuje bardziej przewidywalnym i konsekwentne zachowanie.

Jako ostatni wstęp, należy zauważyć, że PHP używa zliczania referencji i kopiowania przy zapisie do zarządzania pamięcią. Oznacza to, że jeśli "skopiujesz" wartość, po prostu ponownie użyjesz starej wartości i zwiększysz jej liczbę referencji (refcount). Dopiero po wykonaniu jakiejś modyfikacji zostanie wykonana prawdziwa Kopia (zwana "duplikacją"). Zobacz jesteś okłamywany aby uzyskać bardziej obszerne wprowadzenie na ten temat.

PHP 5

Wewnętrzny wskaźnik tablicy i HashPointer

Tablice w PHP 5 mają jeden dedykowany "wskaźnik tablicy wewnętrznej" (IAP), który poprawnie obsługuje modyfikacje: za każdym razem, gdy element zostanie usunięty, będzie sprawdzane, czy IAP wskazuje na ten element. Jeśli tak się stanie, zostanie on przeniesiony do następnego elementu.

Podczas gdy foreach używa IAP, istnieje dodatkowa komplikacja: istnieje tylko jeden IAP, ale jedna tablica może być częścią wielu foreach pętle:

// 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) {
        // ...
    }
}

Aby obsługiwać dwie pętle jednocześnie z tylko jednym wewnętrznym wskaźnikiem tablicy, foreach wykonuje następujące manewry: zanim ciało pętli zostanie wykonane, foreach utworzy kopię zapasową wskaźnika do bieżącego elementu i jego hash do per-foreach HashPointer. Po uruchomieniu ciała pętli, IAP zostanie ustawiony z powrotem do tego elementu, jeśli nadal istnieje. Jeśli jednak element został usunięty, użyjemy go wszędzie tam, gdzie jest aktualnie IAP. Ten schemat przeważnie-niby-działa, ale jest wiele dziwnych zachowań, które możesz z tego wyciągnąć, niektóre z nich zademonstruję poniżej.

Duplikacja tablicy

IAP jest widoczną cechą tablicy (eksponowaną przez rodzinę funkcji current), ponieważ takie zmiany w IAP liczą się jako modyfikacje w semantyce kopiowania przy zapisie. To, niestety, oznacza, że foreach jest w wielu przypadkach zmuszona do powielania tablicy, nad którą się iteruje. Dokładne warunki to:

  1. tablica nie jest reference (is_ref=0). Jeśli jest to odniesienie, to zmiany w nim są przypuszczalnie propagować, więc nie powinno być powielane.
  2. tablica ma refcount>1. Jeśli refcount jest 1, to tablica nie jest współdzielona i możemy ją modyfikować bezpośrednio.

Jeśli tablica nie jest duplikowana (is_ref=0, refcount=1), to tylko jej refcount zostanie zwiększona (*). Dodatkowo, jeśli używana jest foreach przez odniesienie, to (potencjalnie zduplikowana) tablica zostanie zamieniona w Referencja.

Rozważ ten kod jako przykład, w którym następuje duplikacja:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Tutaj, $arr zostaną zduplikowane, aby zapobiec zmianom IAP na $arr przed wyciekiem do $outerArr. W powyższych warunkach tablica nie jest referencją (is_ref=0) i jest używana w dwóch miejscach (refcount=2). Ten wymóg jest niefortunny i artefakt nieoptymalnej implementacji (nie ma tu obaw o modyfikację podczas iteracji, więc tak naprawdę nie musimy używać IAP w miejsce).

(*) zwiększanie refcount tutaj brzmi nieszkodliwie, ale narusza semantykę copy-on-write (COW): oznacza to, że zmodyfikujemy IAP tablicy refcount=2, podczas gdy COW nakazuje, że modyfikacje mogą być wykonywane tylko na wartościach refcount=1. To naruszenie skutkuje widoczną przez użytkownika zmianą zachowania (podczas gdy krowa jest normalnie przezroczysta), ponieważ zmiana IAP na iterowanej tablicy będzie obserwowalna -- ale tylko do pierwszej modyfikacji Nie-IAP na tablicy. Zamiast tego, trzy" poprawne " opcje to: a) zawsze duplikować, b) nie zwiększać refcount i tym samym pozwalając na arbitralną modyfikację iterowanej tablicy w pętli lub c) w ogóle nie używać IAP (rozwiązanie PHP 7).

Kolejność awansowania pozycji

Jest jeszcze jeden szczegół implementacji, o którym należy pamiętać, aby poprawnie zrozumieć poniższe przykłady kodu. "Normalny" sposób zapętlania przez jakąś strukturę danych wyglądałby mniej więcej tak w pseudocode:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}
Jednak, będąc dość wyjątkowym płatkiem śniegu, decyduje się robić rzeczy nieco inaczej:]}
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Mianowicie, wskaźnik tablicy jest już przesunięty do przodu zanim ciało pętli zostanie uruchomione. Oznacza to, że podczas gdy ciało pętli działa na elemencie $i, IAP jest już na elemencie $i+1. Jest to powód, dla którego próbki kodu pokazujące modyfikację podczas iteracji zawsze będą unset następny element, a nie bieżący jeden.

Przykłady: Twoje przypadki testowe

Trzy opisane powyżej aspekty powinny dostarczyć w większości pełnego wrażenia idiosynkrazji implementacji foreach i możemy przejść do omówienia niektórych przykładów.

Zachowanie Twoich przypadków testowych jest proste do wyjaśnienia w tym momencie:

  • W testowych przypadkach 1 i 2 $array zaczyna się od refcount=1, więc nie będzie duplikowany przez foreach: tylko refcount jest zwiększany. Kiedy ciało pętli następnie modyfikuje tablicę (która ma refcount=2 w tym punkcie), duplikacja nastąpi w tym punkcie. Foreach będzie kontynuował pracę nad niezmodyfikowaną kopią $array.

  • W przypadku testowym 3, tablica po raz kolejny nie jest duplikowana, więc foreach będzie modyfikować IAP zmiennej $array. Na końcu iteracji, IAP jest NULL (co oznacza, że iteracja została wykonana), co each wskazuje zwracając false.

  • W przypadkach testowych 4 i 5 zarówno each, jak i reset są funkcjami by-reference. $array ma refcount=2 kiedy jest przekazywana do nich, więc musi być duplikowana. W związku z tym foreach będzie ponownie pracować nad oddzielną tablicą.

Przykłady: efekty current w foreach

Dobrym sposobem na pokazanie różnych zachowań powielania jest obserwowanie zachowania funkcji current() wewnątrz pętli foreach. Rozważmy ten przykład:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

Tutaj powinieneś wiedzieć, że current() jest funkcją by-ref (właściwie: preferuj-ref), mimo że nie modyfikuje tablicy. Musi być tak, aby grać ładnie ze wszystkimi innymi funkcjami, takimi jak next, które są wszystkie przez-ref. By-reference passing implikuje, że tablica musi być oddzielona, a zatem $array i foreach-array będą różne. Powód, dla którego otrzymujesz 2 zamiast 1 jest również wymieniony powyżej: foreach przesuwa wskaźnik tablicy przed uruchomieniem kodu użytkownika, a nie po. Tak więc, mimo że kod znajduje się w pierwszym elemencie, foreach już przesunąłem wskaźnik do drugiego.

Teraz spróbujmy małej modyfikacji:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Tutaj mamy przypadek is_ref=1, więc tablica nie jest kopiowana (tak jak powyżej). Ale teraz, gdy jest to Referencja, tablica nie musi być już powielana podczas przechodzenia do funkcji by-ref current(). Tak więc current() i foreach działają na tej samej tablicy. Nadal jednak widzisz zachowanie "off-by-one", ze względu na sposób, w jaki {18]} przesuwa wskaźnik.

Masz to samo zachowanie, gdy doing by-ref iteration:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Ważną częścią jest to, że foreach uczyni $array is_ref = 1, gdy będzie iteracją przez odniesienie, więc zasadniczo masz taką samą sytuację jak powyżej.

Kolejna mała odmiana, tym razem przypiszemy tablicę do innej zmiennej:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

Tutaj refcount $array wynosi 2, gdy pętla jest uruchomiona, więc raz naprawdę musimy zrobić duplikację z góry. Tak więc $array i tablica używana przez foreach będzie zupełnie od początku. Dlatego otrzymujesz pozycję IAP wszędzie tam, gdzie była przed pętlą (w tym przypadku była na pierwszej pozycji).

Przykłady: modyfikacja podczas iteracji

Próba uwzględnienia modyfikacji podczas iteracji jest miejscem, gdzie powstały wszystkie nasze problemy z foreach, więc warto rozważyć kilka przykładów w tym przypadku.

Rozważ te zagnieżdżone pętle na tej samej tablicy (gdzie iteracja by-ref jest używana, aby upewnić się, że naprawdę jest ten sam):

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)

Oczekiwaną częścią jest to, że (1, 2) brakuje na wyjściu, ponieważ element 1 został usunięty. Prawdopodobnie nieoczekiwane jest to, że zewnętrzna pętla zatrzymuje się po pierwszym elemencie. Dlaczego?

Powodem tego jest opisany powyżej hack zagnieżdżonej pętli: zanim ciało pętli zostanie uruchomione, aktualna pozycja IAP i hash są archiwizowane do HashPointer. Po ciele pętli zostanie przywrócony, ale tylko wtedy, gdy element nadal istnieje, w przeciwnym razie obecna pozycja IAP (cokolwiek to może być) jest używana zamiast. W powyższym przykładzie jest to dokładnie przypadek: bieżący element zewnętrznej pętli został usunięty, więc użyje IAP, który został już oznaczony jako zakończony przez wewnętrzną pętlę!

Kolejną konsekwencją mechanizmu HashPointer backup+restore jest to, że zmiany w IAP poprzez reset() itd. zazwyczaj nie mają wpływu foreach. Na przykład poniższy kod wykonuje się tak, jakby reset() nie były obecne w wszystkie:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

Powodem jest to, że podczas gdy reset() tymczasowo modyfikuje IAP, zostanie on przywrócony do bieżącego elementu foreach po ciele pętli. Aby zmusić reset() do wywołania efektu na pętli, musisz dodatkowo usunąć bieżący element, aby mechanizm kopii zapasowej / przywracania nie zadziałał:

$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

Ale te przykłady są nadal przy zdrowych zmysłach. Prawdziwa zabawa zaczyna się, jeśli pamiętasz, że HashPointer restore używa wskaźnika do elementu i jego hash, aby określić, czy nadal istnieje. Ale: Hasze mają kolizje, a wskaźniki mogą być ponownie użyte! Oznacza to, że przy starannym wyborze klawiszy tablicy możemy sprawić, że foreach uwierzy, że element, który został usunięty nadal istnieje, więc przeskoczy bezpośrednio do niego. Przykład:

$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

Tutaj powinniśmy normalnie oczekiwać wyjścia 1, 1, 3, 4 zgodnie z poprzednimi regułami. Jak to się dzieje, że 'FYFY' ma ten sam hash co usunięty element 'EzFY', a alokator ponownie wykorzystuje tę samą lokalizację pamięci do przechowywania żywioł. Tak więc foreach kończy się bezpośrednio skacząc do nowo wstawionego elementu, a tym samym skracając pętlę.

Zastępowanie iteracyjnej jednostki podczas pętli

Ostatni dziwny przypadek, o którym chciałbym wspomnieć, to to, że PHP pozwala na zastąpienie iteracji encji podczas pętli. Możesz więc zacząć iterację na jednej tablicy, a następnie zastąpić ją inną tablicą w połowie. Można też rozpocząć iterację na tablicy, a następnie zastąpić ją obiektem:

$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 */

As widać, że w tym przypadku PHP po prostu rozpocznie iterację drugiego elementu Od początku, gdy nastąpi podstawienie.

PHP 7

Iteratory Hashtable

Jeśli nadal pamiętasz, głównym problemem iteracji tablicy było to, jak poradzić sobie z usuwaniem elementów w połowie iteracji. PHP 5 używało do tego celu pojedynczego wskaźnika tablicy wewnętrznej (IAP), który był nieco nieoptymalny, ponieważ jeden wskaźnik tablicy musiał być rozciągnięty, aby obsługiwać wiele jednoczesnych pętli foreach i interakcja z reset() itd. na dodatek.

PHP 7 używa innego podejścia, a mianowicie obsługuje tworzenie dowolnej ilości zewnętrznych, bezpiecznych iteratorów hashtable. Iteratory te muszą być zarejestrowane w tablicy, z której punktu mają taką samą semantykę jak IAP: jeśli element tablicy zostanie usunięty, wszystkie Iteratory z hashtable wskazujące na ten element zostaną przeniesione do następnego elementu.

Oznacza to, że foreach nie będzie już używać IAP w ogóle . Pętla foreach nie będzie miała żadnego wpływu na wyniki current() itd. a na jego własne zachowanie nigdy nie będą miały wpływu takie funkcje jak reset() itp.

Duplikacja tablicy

Kolejna ważna zmiana pomiędzy PHP 5 i PHP 7 dotyczy duplikacji tablicy. Teraz, gdy IAP nie jest już używany, iteracja tablicy według wartości spowoduje tylko zwiększenie refcount (zamiast powielania tablicy) we wszystkich przypadkach. Jeśli tablica zostanie zmodyfikowana podczas pętli foreach, w w tym punkcie nastąpi duplikacja (zgodnie z copy-on-write) i foreach będzie nadal działać na starej tablicy.

W większości przypadków zmiana ta jest przejrzysta i nie ma innego wpływu niż lepsza wydajność. Jednak jest jedna okazja, w której powoduje to inne zachowanie, a mianowicie przypadek, w którym tablica była wcześniej referencją: {]}

$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 */

Poprzednio iteracja według wartości tablicy referencyjnej była szczególnymi przypadkami. W tym przypadku nie doszło do powielania, więc wszystkie modyfikacje tablicy podczas iteracji będą odzwierciedlane przez pętlę. W PHP 7 ten szczególny przypadek znikł: iteracja według wartości tablicy będzie zawsze pracować nad oryginalnymi elementami, pomijając wszelkie modyfikacje podczas pętli.

To oczywiście nie dotyczy iteracji by-reference. Jeśli iterujesz przez odniesienie, wszystkie modyfikacje zostaną odzwierciedlone przez pętlę. Co ciekawe, to samo odnosi się do iteracji po wartości zwykłej obiekty:

$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 */

Odzwierciedla to semantykę obiektów (tzn. zachowują się podobnie jak odniesienia nawet w kontekstach wartości).

Przykłady

Rozważmy kilka przykładów, zaczynając od przypadków testowych:]}
  • Przypadki testowe 1 i 2 zachowują ten sam wynik: iteracja tablicy według wartości zawsze działa na oryginalnych elementach. (W tym przypadku nawet refcounting i zachowanie duplikacji jest dokładnie takie samo pomiędzy PHP 5 i PHP 7).

  • Zmiana przypadku testowego 3: Foreach nie używa już IAP, więc pętla nie ma wpływu na each(). Będzie miał to samo wyjście przed i po.

  • Przypadki testowe 4 i 5 pozostają takie same: each() i reset() powieli tablicę przed zmianą IAP, podczas gdy foreach nadal używa oryginalnej tablicy. (Nie żeby zmiana IAP miała znaczenie, nawet gdyby tablica była współdzielona.)

Drugi zestaw przykładów dotyczył zachowanie current() w różnych konfiguracjach reference/refcounting. To już nie ma sensu, ponieważ current() nie ma wpływu na pętlę, więc jej wartość zwracana zawsze pozostaje taka sama.

Jednak, gdy rozważamy modyfikacje w iteracji, dostajemy kilka interesujących zmian. Mam nadzieję, że znajdziesz nowe zachowanie saner. Pierwszy przykład:

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

Jak widzisz, zewnętrzna pętla nie przerywa już po pierwszej iteracji. Powodem jest to, że obie pętle mają teraz całkowicie oddzielne Iteratory hashtable i nie ma już żadnego skażenia krzyżowego obu pętli poprzez współdzielony IAP.

Kolejny dziwny przypadek krawędzi, który jest teraz naprawiony, jest dziwnym efektem, który uzyskujesz, gdy usuwasz i dodajesz elementy, które mają ten sam 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

Poprzednio mechanizm przywracania Hashpointera przeskoczył bezpośrednio do nowego elementu, ponieważ "wyglądał" tak, jakby był taki sam jak usunięty element (ze względu na kolizję hash i wskaźnika). Ponieważ nie polegamy już na element hash dla czegokolwiek, nie jest to już problemem.

 1714
Author: NikiC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-01 21:32:56

W przykładzie 3 nie modyfikujemy tablicy. We wszystkich innych przykładach modyfikuje się zawartość lub wewnętrzny wskaźnik tablicy. Jest to ważne, jeśli chodzi o tablice PHP ze względu na semantykę operatora przydziału.

Operator przypisania tablic w PHP działa bardziej jak leniwy klon. Przypisanie jednej zmiennej do innej, która zawiera tablicę, sklonuje tablicę, w przeciwieństwie do większości języków. Jednak rzeczywiste klonowanie nie zostanie wykonane, chyba że jest to konieczne. Oznacza to, że klonowanie nastąpi tylko wtedy, gdy jedna ze zmiennych zostanie zmodyfikowana (copy-on-write).

Oto przykład:

$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.

Wracając do Twoich testów, możesz sobie wyobrazić, że foreach tworzy jakiś iterator z odniesieniem do tablicy. To odniesienie działa dokładnie tak jak zmienna $b w moim przykładzie. Jednak iterator wraz z referencją żyje tylko podczas pętli, a następnie oba są odrzucane. Teraz widać, że we wszystkich przypadkach oprócz 3, tablica jest modyfikowana podczas pętli, podczas gdy to dodatkowe odniesienie jest żywe. To wyzwala klona, a to wyjaśnia, co tu się dzieje!

Oto doskonały artykuł na kolejny efekt uboczny tego zachowania kopiowania przy zapisie: Operator trójwarstwowy PHP: szybko czy nie?

 120
Author: linepogl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-15 11:10:06

Niektóre punkty, na które należy zwrócić uwagę podczas pracy z foreach():

A) foreach działa na prospektowanej kopii oryginalnej tablicy. Oznacza to, że foreach() będzie miał współdzielone przechowywanie danych do czasu, aż prospected copy będzie not created foreach Notes / User comments .

B) co uruchamia kopię prospektu? Kopia jest tworzona na podstawie polityki copy-on-write, czyli gdy tablica przekazywana do foreach() jest zmieniana, klon oryginalnej tablicy jest stworzony.

C) oryginalna tablica i iterator foreach() będą miały DISTINCT SENTINEL VARIABLES, czyli jeden dla oryginalnej tablicy, a drugi dla foreach; Zobacz kod testu poniżej. SPL , Iteratory i Array Iterator.

Pytanie o przepełnienie stosu Jak upewnić się, że wartość jest resetowana w pętli 'foreach' w PHP? odnosi się do przypadków (3,4,5) twojego pytania.

Poniższy przykład pokazuje, że each() I reset () nie wpływają na SENTINEL zmienne (for example, the current index variable) z iteratora foreach().

$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/>";

Wyjście:

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
 54
Author: sakhunzai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-04-23 21:57:45

UWAGA DLA PHP 7

Aby zaktualizować tę odpowiedź, ponieważ zyskała pewną popularność: ta odpowiedź nie ma już zastosowania od PHP 7. Jak wyjaśniono w " backward incompatible changes ", W PHP 7 foreach działa na kopii tablicy, więc wszelkie zmiany w samej tablicy nie są odzwierciedlane w pętli foreach. Więcej szczegółów pod linkiem.

Wyjaśnienie (cytat z php.net):

Pierwsza postać pętli nad tablicą podaną przez array_expression. Na każdym iteracji, wartość bieżącego elementu jest przypisana do $value i wewnętrzny wskaźnik tablicy jest rozwijany o jeden (tak na następnym iteracji, będziesz patrzył na następny element).

Tak więc, w pierwszym przykładzie masz tylko jeden element w tablicy, a gdy wskaźnik jest przesunięty następny element nie istnieje, więc po dodaniu nowego elementu foreach kończy się, ponieważ już "zdecydował", że to jako ostatni element.

In your second przykład, zaczynasz od dwóch elementów, a pętla foreach nie znajduje się na ostatnim elemencie, więc oblicza tablicę podczas następnej iteracji i w ten sposób uświadamia sobie, że w tablicy znajduje się nowy element.

Uważam, że to wszystko jest konsekwencją na każdej iteracji części wyjaśnienia w dokumentacji, co prawdopodobnie oznacza, że foreach robi całą logikę, zanim wywoła kod w {}.

Test case

Jeśli uruchomisz to:

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

Dostaniesz to wyjście:

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

Co oznacza, że zaakceptowała modyfikację i przeszła przez nią, ponieważ została zmodyfikowana "w czasie". Ale jeśli to zrobisz:

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

Otrzymasz:

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

Co oznacza, że tablica została zmodyfikowana, ale ponieważ zmodyfikowaliśmy ją, gdy foreach była już na ostatnim elemencie tablicy, "zdecydowała", że nie będzie już pętli i mimo że dodaliśmy nowy element, dodaliśmy go "za późno" i nie został zapętlony.

Szczegółowe wyjaśnienie można przeczytać na Jak działa PHP 'foreach'? co wyjaśnia wewnętrzne przyczyny tego zachowania.

 38
Author: dkasipovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-01 12:23:44

Zgodnie z dokumentacją dostarczoną przez podręcznik PHP.

Przy każdej iteracji wartość bieżącego elementu jest przypisana do $v, a wewnętrzna
wskaźnik tablicy jest rozszerzany o jeden (więc przy następnej iteracji będziesz patrzył na następny element).

Tak jak w Twoim pierwszym przykładzie:

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

$array ma tylko jeden element, więc zgodnie z wykonaniem foreach, 1 Przypisz do $v i nie ma żadnego innego elementu do przesunięcia wskaźnika

Ale w Twoim drugi przykład:

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

$array mają dwa elementy, więc teraz $array ocenia indeksy zerowe i przesuwa wskaźnik o jeden. Dla pierwszej iteracji pętli, Dodano $array['baz']=3; jako pass by reference.

 16
Author: user3535130,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-15 09:35:09

Świetne pytanie, ponieważ wielu programistów, nawet tych doświadczonych, jest zdezorientowanych sposobem, w jaki PHP obsługuje tablice w pętlach foreach. W standardowej pętli foreach PHP tworzy kopię tablicy używanej w pętli. Kopia jest odrzucana natychmiast po zakończeniu pętli. Jest to przezroczyste w działaniu prostej pętli foreach. Na przykład:

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

To wyjście:

apple
banana
coconut

Więc kopia jest tworzona, ale programista nie zauważa, ponieważ oryginalna tablica nie jest odwołuje się w pętli lub po jej zakończeniu. Jednak, gdy próbujesz zmodyfikować elementy w pętli, okazuje się, że są one niezmodyfikowane po zakończeniu:

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

print_r($set);

To wyjście:

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

Żadnych zmian z oryginału nie można zauważyć, w rzeczywistości nie ma żadnych zmian z oryginału, mimo że wyraźnie przypisano wartość do $ item. Dzieje się tak dlatego, że operujesz na $ item tak, jak jest to widoczne w kopii $set, nad którą pracujesz. Możesz to nadpisać, chwytając $item przez odniesienie, jak tak:

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

To wyjście:

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

Jest więc oczywiste i możliwe do zaobserwowania, gdy $item jest operowane przez odniesienie, zmiany wprowadzone do $item są wprowadzane do członków oryginalnego $set. Użycie $item przez odniesienie również uniemożliwia PHP tworzenie kopii tablicy. Aby to przetestować, najpierw pokażemy szybki skrypt demonstrujący kopię:

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

To wyjście:

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

Jak pokazano w przykładzie, PHP skopiowało $set i użyło go do pętli, ale gdy zmienna $set została użyta wewnątrz pętli, PHP dodało zmienne do oryginalnej tablicy, a nie do skopiowanej tablicy. Zasadniczo PHP używa tylko skopiowanej tablicy do wykonania pętli i przypisania $item. Z tego powodu powyższa pętla wykonuje tylko 3 razy i za każdym razem dodaje inną wartość na koniec oryginalnego $set, pozostawiając oryginalny $set z 6 elementami, ale nigdy nie wchodząc do nieskończonej pętli.

Jednak, co by było, gdybyśmy użyli $item przez odniesienie, jak wspomniałem wcześniej? Pojedynczy znak dodany do powyższego testu:

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

Daje nieskończoną pętlę. Zauważ, że w rzeczywistości jest to nieskończona pętla, będziesz musiał samodzielnie zabić skrypt lub poczekać, aż Twój system operacyjny skończy się z pamięcią. Dodałem następujący wiersz do mojego skryptu, aby PHP bardzo szybko wyczerpało pamięć, proponuję zrobić to samo, jeśli masz zamiar uruchomić te testy nieskończonej pętli: {]}

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

Więc w poprzednim przykładzie z pętlą nieskończoną widzimy powód, dla którego PHP został napisany w celu utworzenia kopii tablicy do pętli. Gdy kopia jest tworzona i używana tylko przez samą strukturę pętli, tablica pozostaje statyczna przez cały czas wykonywania pętli, więc nigdy nie napotkasz problemów.

 14
Author: Hrvoje Antunović,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-21 08:44:53

Pętla PHP foreach może być używana z Indexed arrays, Associative arrays i Object public variables.

W pętli foreach pierwszą rzeczą, jaką robi php, jest to, że tworzy kopię tablicy, która ma być iterowana. PHP następnie iteruje nad tą nową copy tablicy zamiast oryginalnej. Jest to pokazane w poniższym przykładzie:

<?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).

Poza tym php pozwala również na użycie iterated values as a reference to the original array value. Jest to pokazane poniżej:

<?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

Uwaga: nie pozwala original array indexes używać jako references.

Źródło: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

 8
Author: Pranav Rana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-13 14:08:45