Pytanie Jak działa "foreach" PHP?


Pozwolę sobie to poprzedzić, mówiąc, że wiem co foreach jest, robi i jak z niego korzystać. To pytanie dotyczy tego, jak działa pod maską, i nie chcę żadnych odpowiedzi na wzór "w ten sposób zapętlasz tablicę z foreach".


Przez długi czas zakładałem, że foreach pracował z samą tablicą. Potem znalazłem wiele odniesień do faktu, że działa z Kopiuj tablicy, i od tego czasu założyłem, że to koniec historii. Ale ostatnio wdałem się w dyskusję na ten temat i po krótkim doświadczeniu okazało się, że nie jest to w 100% prawdziwe.

Pozwól mi pokazać, co mam na myśli. W przypadku 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 macierzą źródłową - w przeciwnym razie pętla będzie trwać wiecznie, ponieważ nieustannie pchamy elementy do tablicy podczas pętli. Ale żeby się upewnić, że 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 zmienilibyśmy wartości podczas pętli. Ale...

Jeśli spojrzymy w podręcznik, stwierdzamy to stwierdzenie:

Po pierwszym uruchomieniu foreach wewnętrzny wskaźnik tablicy jest automatycznie resetowany do pierwszego elementu tablicy.

Racja ... wydaje się to sugerować foreach opiera się na wskaźniku tablicowym tablicy źródłowej. Ale właśnie udowodniliśmy, że jesteśmy nie działa z tablicą źródłową, dobrze? Cóż, 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 tego, że nie pracujemy bezpośrednio z macierzą źródłową, pracujemy bezpośrednio ze wskaźnikiem źródłowym - pokazuje to fakt, że wskaźnik znajduje się na końcu tablicy na końcu pętli. Tylko że to nie może być prawdą - jeśli tak, to wtedy Przypadek testowy 1 Pętla na zawsze.

Podręcznik PHP również stwierdza:

Ponieważ foreach polega na wewnętrznej tablicy wskaźnika, zmiana jej w pętli może doprowadzić do nieoczekiwanego zachowania.

Cóż, dowiedzmy się, co to "nieoczekiwane zachowanie" (technicznie, 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 tam nieoczekiwanego, w rzeczywistości wydaje się wspierać teorię "kopii źródła".


Pytanie

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

Wydaje mi się, że foreach współpracuje z Kopiuj tablicy, ale ustawia wskaźnik tablicy z tablicy źródłowej na koniec tablicy po pętli.

  • Czy to prawda i cała historia?
  • Jeśli nie, to co tak naprawdę robi?
  • Czy istnieje jakakolwiek sytuacja, w której używa się funkcji, które dostosowują wskaźnik tablicy (each(), reset() et al.) podczas a foreach może wpłynąć na wynik pętli?

1644
2018-04-07 19:33


pochodzenie


@DaveRandom There's a php-internals tag powinien prawdopodobnie iść, ale pozostawiam ci decyzję, jeśli którykolwiek z pozostałych 5 tagów do zastąpienia. - Michael Berkowski
wygląda jak COW, bez usuwania uchwytu - zb'
Na początku pomyślałem »Boże, kolejne pytanie dla początkujących. Przeczytaj dokumenty ... hm, wyraźnie niezdefiniowane zachowanie «. Potem przeczytałem pełne pytanie i muszę powiedzieć: lubię to. Włożyłeś w to sporo wysiłku i spisałeś wszystkie testcases. ps. czy test 4 i 5 są takie same? - knittl
Wystarczy pomyśleć o tym, dlaczego ma sens, aby wskaźnik tablicy został dotknięty: PHP musi zresetować i przesunąć wewnętrzny wskaźnik tablicy oryginalnej tablicy wraz z kopią, ponieważ użytkownik może poprosić o odniesienie do bieżącej wartości (foreach ($array as &$value)) - PHP musi znać aktualną pozycję w oryginalnej tablicy, nawet jeśli jest to iteracja po kopii. - Niko
@Sean: IMHO, dokumentacja PHP jest naprawdę bardzo zła w opisywaniu niuansów podstawowych funkcji językowych. Ale może tak jest, ponieważ tak wiele specjalnych przypadków ad hoc upieczonych jest w języku ... - Oliver Charlesworth


Odpowiedzi:


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

Poniżej postaram się wyjaśnić, jak dokładnie działa iteracja w różnych przypadkach. Zdecydowanie najprostszym przypadkiem są Traversable obiekty, jak te foreach jest zasadniczo tylko cukrem składniowym dla kodu wzdłuż tych linii:

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

W przypadku klas wewnętrznych można uniknąć rzeczywistych wywołań metod, korzystając z wewnętrznego interfejsu API, który zasadniczo odzwierciedla rzeczywisty interfejs Iterator interfejs 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ą naprawdę uporządkowanymi słownikami i będą one przechodzić zgodnie z tą kolejnością (która pasuje do kolejności wstawiania, o ile nie używasz czegoś takiego jak sort). Jest to sprzeczne z iterowaniem przez naturalną kolejność kluczy (jak często działają listy w innych językach) lub w ogóle bez zdefiniowanego porządku (jak często działają słowniki w innych językach).

To samo dotyczy obiektów, ponieważ właściwości obiektu mogą być postrzegane jako inne (uporządkowane) nazwy właściwości mapowania słownika do ich wartości, a także do obsługi 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 iterowanie 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 omawiam tutaj zbyt wiele iteracji zwykłego obiektu).

Jak na razie dobrze. Iterowanie przez słownik nie może być zbyt trudne, prawda? Problemy zaczynają się, kiedy zdajesz sobie sprawę, że tablica / obiekt może się zmieniać podczas iteracji. Jest na to wiele sposobów:

  • Jeśli wykonujesz iterację przez odniesienie używając foreach ($arr as &$v) następnie $arr jest przekształcany w referencję i możesz go zmienić podczas iteracji.
  • W PHP 5 to samo dotyczy nawet iteracji według wartości, ale tablica była wcześniej punktem odniesienia: $ref =& $arr; foreach ($ref as $v)
  • Obiekty przechodzą semantykę przy przechodzeniu przez klamkę, co dla praktycznych celów oznacza, że ​​zachowują się jak referencje. Dlatego obiekty mogą być zawsze zmieniane podczas iteracji.

Problem z zezwoleniem na modyfikacje podczas iteracji ma miejsce w przypadku, gdy element, na którym się obecnie znajdujesz, zostanie usunięty. Powiedzmy, że używasz wskaźnika, aby śledzić element tablicy, w którym aktualnie się znajdujesz. Jeśli ten element jest teraz uwolniony, pozostaje ci wskaźnik zwisający (zwykle powodujący błąd segfault).

Istnieją różne sposoby rozwiązania tego problemu. PHP 5 i PHP 7 różnią się znacznie pod tym względem i opiszę oba te zachowania. Podsumowując, podejście PHP 5 było raczej głupie i prowadziło do wszelkiego rodzaju dziwnych problemów z krawędziami, podczas gdy podejście bardziej zaangażowane w PHP 7 zapewnia bardziej przewidywalne i spójne zachowanie.

Na koniec, należy zauważyć, że PHP używa liczenia referencji i kopiowania przy zapisie do zarządzania pamięcią. Oznacza to, że jeśli "skopiujesz" wartość, to po prostu ponownie wykorzystasz starą wartość i zwiększysz jej liczbę referencyjną (refcount). Dopiero po wykonaniu jakiejś modyfikacji zostanie wykonana prawdziwa kopia (zwana "duplikacją"). Widzieć Jesteś okłamywany dla szerszego wprowadzenia na ten temat.

PHP 5

Wewnętrzny wskaźnik tablicy i HashPointer

Tablice w PHP 5 mają jeden dedykowany "wewnętrzny wskaźnik tablicy" (IAP), który odpowiednio obsługuje modyfikacje: Za każdym razem, gdy element zostanie usunięty, będzie sprawdzić, czy IAP wskazuje na ten element. Jeśli tak, to zamiast tego przechodzi do następnego elementu.

Mimo że foreach korzysta z IAP, istnieje dodatkowa komplikacja: istnieje tylko jeden IAP, ale jedna tablica może być częścią wielu pętli foreach:

// 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łużyć dwie równoczesne pętle z tylko jednym wewnętrznym wskaźnikiem tablicy, foreach wykonuje następujące schenanigany: przed wykonaniem korpusu pętli foreach zrób kopię wskaźnika do bieżącego elementu i jego skrótu do per-foreach HashPointer. Po uruchomieniu treści pętli, IAP zostanie przywrócony do tego elementu, jeśli nadal istnieje. Jeśli jednak element został usunięty, użyjemy go tylko wtedy, gdy IAP jest aktualnie w użyciu. Ten schemat głównie-kinda-sortof działa, ale jest wiele dziwnych zachowań, z których możesz wyjść, niektóre z nich pokażę poniżej.

Powielanie tablic

IAP jest widoczną cechą tablicy (odsłoniętej przez current rodzina funkcji), jako że zmiany w IAP liczą się jako modyfikacje w ramach semantyki "copy-on-write". To niestety oznacza, że ​​foreach jest w wielu przypadkach zmuszony do duplikowania tablicy, którą iteruje. Dokładne warunki to:

  1. Tablica nie jest odniesieniem (is_ref = 0). Jeśli jest to odniesienie, to zmienia się w nim domniemany propagować, więc nie powinno się go powielać.
  2. Tablica ma wartość refcount> 1. Jeśli wartość refcount wynosi 1, tablica nie jest udostępniana i możemy ją bezpośrednio modyfikować.

Jeśli tablica nie jest zduplikowana (is_ref = 0, refcount = 1), to tylko jej refcount będzie inkrementowany (*). Dodatkowo, jeśli użyto foreach przez odniesienie, wówczas (potencjalnie zduplikowana) tablica zostanie przekształcona w odniesienie.

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

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

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

Tutaj, $arr zostanie zduplikowany, aby zapobiec wprowadzaniu zmian IAP $arr od wycieku do $outerArr. Pod względem powyższych warunków tablica nie jest odniesieniem (is_ref = 0) i jest używana w dwóch miejscach (refcount = 2). Wymóg ten jest niefortunny i jest artefaktem nieoptymalnej implementacji (nie ma obaw o modyfikację podczas iteracji tutaj, więc naprawdę nie potrzebujemy używać IAP w pierwszej kolejności).

(*) Inkrementacja refcountu tutaj brzmi niewinnie, ale narusza semantykę copy-on-write (COW): Oznacza to, że będziemy modyfikować IAP tablicy refcount = 2, podczas gdy COW dyktuje, że modyfikacje mogą być wykonywane tylko na refcount = 1 wartości. Naruszenie to powoduje widoczną zmianę zachowania użytkownika (podczas gdy COW jest zwykle przezroczysty), ponieważ zmiana IAP w iterowanej macierzy będzie możliwa do zaobserwowania - ale tylko do pierwszej modyfikacji nie-IAP w macierzy. Zamiast tego, trzy "poprawne" opcje byłyby: a) zawsze duplikować, b) nie zwiększać refkonta i tym samym pozwalać na modyfikację arbitralnej tablicy w pętli, lub c) w ogóle nie korzystać z IAP ( rozwiązanie PHP 7).

Ustaw poziom zaawansowania

Istnieje jeden ostatni szczegół implementacji, który musisz znać, aby poprawnie zrozumieć poniższe przykłady kodu. "Normalny" sposób przechodzenia przez pewną strukturę danych wyglądałby tak: pseudokod:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

jednak foreach, będąc raczej specjalnym płatkiem śniegu, wybiera nieco inne czynności:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Mianowicie wskaźnik tablicy jest już przesunięty do przodu przed ciało pętli działa. Oznacza to, że podczas gdy ciało pętli działa na elemencie $i, IAP jest już w elemencie $i+1. To jest powód, dla którego próbki kodu pokazujące modyfikację podczas iteracji będą zawsze wyłączać Kolejny element, a nie bieżący.

Przykłady: Twoje przypadki testowe

Opisane powyżej trzy aspekty powinny dostarczyć w większości pełnego obrazu idiosynkrazji implementacji foreach i możemy przejść do omówienia kilku przykładów.

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

  • W przypadkach testowych 1 i 2 $array rozpoczyna się od refcount = 1, więc nie zostanie zduplikowane przez foreach: tylko refcount jest inkrementowany. Kiedy ciało pętli modyfikuje następnie 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, po raz kolejny tablica nie jest duplikowana, a zatem foreach będzie modyfikować IAP $array zmienna. Pod koniec iteracji IAP ma wartość NULL (czyli iteracja wykonana), która each wskazuje przez powrót false.

  • W przypadkach testowych 4 i 5 oba each i reset są funkcjami odsyłaczy. The $array ma refcount=2 kiedy zostanie im przekazany, więc musi zostać zduplikowany. Takie jak foreach będzie pracować ponownie na oddzielnej tablicy.

Przykłady: efekty current w foreach

Dobrym sposobem na pokazanie różnych zachowań związanych z duplikacją jest obserwowanie zachowań current() funkcja 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ś to wiedzieć current() jest funkcją by-ref (właściwie: prefer-ref), nawet jeśli nie modyfikuje tablicy. Musi być tak, aby grać dobrze z wszystkimi innymi funkcjami, takimi jak next które są wszystkie by-ref. Przekazywanie przez odniesienie oznacza, że ​​tablica musi zostać oddzielona, ​​a tym samym $array a tablica foreach będzie inna. Powód, który otrzymujesz 2 zamiast 1 jest również wymieniony powyżej: foreach przesuwa wskaźnik tablicy przed uruchomienie kodu użytkownika, nie po. Więc nawet jeśli kod znajduje się na pierwszym elemencie, foreach przesunie już 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 odniesienie, tablica nie musi już być powielana podczas przechodzenia do by-ref current() funkcjonować. A zatem current() a foreach działa na tej samej tablicy. Nadal jednak widzisz zachowanie "od zera", ze względu na sposób foreachprzesuwa wskaźnik.

Takie samo zachowanie występuje podczas iteracji by-ref:

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

Tutaj ważną częścią jest to, co zrobią foreach $array a is_ref = 1, gdy jest iterowane przez odniesienie, więc w zasadzie masz taką samą sytuację jak powyżej.

Kolejna niewielka 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 przelicznik $array jest 2, gdy pętla jest uruchomiona, więc na razie musimy zrobić z góry duplikację. A zatem $array a tablica używana przez foreach będzie całkowicie odrębna od samego początku. Dlatego otrzymujesz pozycję IAP, gdziekolwiek była przed pętlą (w tym przypadku była na pierwszej pozycji).

Przykłady: modyfikacja podczas iteracji

Próba uwzględnienia modyfikacji w trakcie iteracji to miejsce, w którym powstały wszystkie problemy związane z Foreach, dlatego warto wziąć pod uwagę kilka przykładów tego przypadku.

Rozważ te zagnieżdżone pętle w tej samej tablicy (gdzie ta iteracja jest używana, aby upewnić się, że rzeczywiście jest taka sama):

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)

Spodziewana część tutaj jest taka (1, 2) brakuje w wyniku, ponieważ element 1 zostało usunięte. Prawdopodobnie nieoczekiwane jest to, że zewnętrzna pętla zatrzymuje się po pierwszym elemencie. Dlaczego?

Powodem tego jest hak zagnieżdżony opisany powyżej: Przed uruchomieniem ciała pętli, aktualna pozycja IAP i skrót są kopiowane do HashPointer. Po ciele pętli zostanie przywrócona, ale tylko wtedy, gdy element nadal istnieje, w przeciwnym razie używana jest aktualna pozycja IAP (cokolwiek może być). W powyższym przykładzie tak właśnie jest: usunięto bieżący element zewnętrznej pętli, więc użyje IAP, który został już oznaczony jako zakończony przez wewnętrzną pętlę!

Kolejna konsekwencja HashPointer Mechanizm backup + restore to zmiany w IAP reset() itp. zwykle nie wpływają na foreach. Na przykład poniższy kod jest wykonywany jak gdyby reset() w ogóle nie były obecne:

$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 przywrócony do bieżącego elementu foreach za ciałem pętli. Zmuszać reset() aby uzyskać efekt w pętli, musisz dodatkowo usunąć bieżący element, aby mechanizm tworzenia kopii zapasowych / przywracania się nie powiódł:

$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 zdrowe. Prawdziwa zabawa zaczyna się, jeśli pamiętasz, że HashPointer restore używa wskaźnika do elementu i jego skrótu, aby określić, czy nadal istnieje. Ale: Hashe mają kolizje, a wskaźniki można ponownie wykorzystać! Oznacza to, że dzięki starannemu wyborowi kluczy tablicowych możemy dokonać foreach wierzę, ż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 zwykle powinniśmy oczekiwać wyniku 1, 1, 3, 4 zgodnie z wcześniejszymi zasadami. Jak to się dzieje 'FYFY' ma taki sam skrót jak usunięty element 'EzFY', a alokator ponownie korzysta z tej samej lokalizacji pamięci, aby przechowywać element. Tak więc foreach kończy się bezpośrednio przeskakując do nowo wstawionego elementu, tym samym skracając pętlę.

Zastępowanie iterowanego obiektu podczas pętli

Ostatnim dziwnym przypadkiem, o którym chciałbym wspomnieć, jest to, że PHP pozwala na zastąpienie iterowanego obiektu podczas pętli. Możesz więc rozpocząć iterację w jednej tablicy, a następnie zastąpić ją inną tablicą w połowie. Lub uruchom iterację w tablicy, a następnie zastąp 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 */

Jak widać w tym przypadku, PHP po prostu zacznie iterować drugi obiekt od początku, gdy nastąpi podstawienie.

PHP 7

Hashtable iteratory

Jeśli nadal pamiętasz, główny problem z iteracją macierzy polegał na tym, jak radzić sobie z usuwaniem elementów w połowie iteracji. PHP 5 używał do tego celu jednego wewnętrznego wskaźnika tablicy (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() itp. na dodatek.

PHP 7 wykorzystuje inne podejście, a mianowicie obsługuje tworzenie dowolnych ilości zewnętrznych, bezpiecznych iteratorów. Te iteratory muszą być zarejestrowane w tablicy, od tego momentu mają one tę samą semantykę co IAP: Jeśli element tablicy zostanie usunięty, wszystkie iteratory typu hashtable wskazują na to, że element zostanie przesunięty 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 absolutnie żadnego wpływu na wyniki current() itp. i jego własne zachowanie nigdy nie będzie miało wpływu na takie funkcje jak reset() itp.

Powielanie tablic

Kolejna ważna zmiana między PHP 5 i PHP 7 dotyczy duplikacji macierzy. Teraz, gdy IAP nie jest już używany, iteracja tablicy wartości bywa tylko we wszystkich przypadkach tylko przyrostem refcount (zamiast duplikowania tablicy). Jeśli tablica zostanie zmodyfikowana podczas pętli foreach, w tym momencie nastąpi duplikacja (zgodnie z copy-on-write), a foreach będzie działał na starej tablicy.

W większości przypadków ta zmiana jest przejrzysta i nie ma żadnego innego skutku niż lepsza wydajność. Jednak jest jedna sytuacja, w której powoduje inne zachowanie, a mianowicie przypadek, w którym tablica była wcześniej punktem odniesienia:

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

Wcześniejsza iteracja tablic referencyjnych była przypadkami specjalnymi. W tym przypadku nie wystąpiło duplikowanie, więc wszystkie modyfikacje tablicy podczas iteracji byłyby odzwierciedlane przez pętlę. W PHP 7 ten wyjątkowy przypadek zniknął: nastąpi iteracja tablicy wartości po wartości zawsze kontynuuj pracę nad oryginalnymi elementami, ignorując wszelkie modyfikacje podczas pętli.

To oczywiście nie ma zastosowania do iteracji referencyjnej. Jeśli dokonasz iteracji przez odniesienie, wszystkie modyfikacje zostaną odzwierciedlone w pętli. Co ciekawe, to samo dotyczy iteracji czystych obiektów o wartościach:

$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 klawesynowych (tzn. Zachowują się jak referencje nawet w kontekstach wartości dodanej).

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 wartości zależnej zawsze działa na oryginalnych elementach. (W tym przypadku nawet policzenie i powielanie zachowań jest dokładnie takie samo między PHP 5 i PHP 7).

  • Zmiany w teście przypadku 3: Foreach nie używa już IAP, więc each() nie ma wpływu na pętlę. Będzie mieć to samo wyjście przed i po.

  • Przypadki testowe 4 i 5 pozostają takie same: each() i reset() spowoduje duplikowanie tablicy przed zmianą IAP, podczas gdy foreach nadal używa oryginalnej tablicy. (Nie znaczy to, że zmiana IAP miałaby znaczenie, nawet jeśli tablica została udostępniona).

Drugi zestaw przykładów związany był z zachowaniem current() w różnych konfiguracjach referencyjnych / przeliczeniowych. To nie ma już sensu, ponieważ current() jest całkowicie niezależny od pętli, więc jego wartość zwracana zawsze pozostaje taka sama.

Jednakże, rozważamy modyfikacje podczas iteracji. Mam nadzieję, że nowe zachowanie będzie bardziej rozsądne. 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 widać, zewnętrzna pętla nie kończy się już po pierwszej iteracji. Powodem jest to, że obie pętle mają teraz całkowicie oddzielne iteratory i nie ma już zanieczyszczenia krzyżowego obu pętli poprzez wspólny IAP.

Kolejny dziwny przypadek krawędzi, który jest teraz naprawiony, to dziwny efekt, jaki można uzyskać po usunięciu i dodaniu elementów, które mają ten sam skrót:

$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 HashPointer przeskoczył w prawo do nowego elementu, ponieważ "wyglądał" tak, jakby był taki sam jak element usuwania (ze względu na kolidujący skrót i wskaźnik). Ponieważ nie polegamy już na haszyle elementu, nie jest to już problemem.


1384
2018-02-13 13:21



@Baba To robi. Przejście do funkcji jest takie samo jak robienie $foo = $array przed pętlą;) - NikiC
Dla tych z was, którzy nie wiedzą, czym jest zval, proszę odnieść się do Sary Goleman blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
@unbeli Używam terminologii używanej wewnętrznie przez PHP. The Buckets są częścią podwójnie połączonej listy dla kolizji mieszania, a także części podwójnie połączonej listy dla zamówienia;) - NikiC
@NikiC: czy naprawdę musisz iść na uniwersytet? Masz tylko 19 lat, ale wyglądasz na dużo bardziej doświadczonego - dynamic
Świetny anwser. Myślę, że miałeś na myśli iterate($outerArr); i nie iterate($arr); gdzieś. - niahoo


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

Operator przypisania dla tablic w PHP działa bardziej jak leniwy klon. Przypisanie jednej zmiennej do drugiej zawierającej tablicę spowoduje sklonowanie tablicy, w przeciwieństwie do większości języków. Jednak faktyczne klonowanie nie zostanie wykonane, chyba że będzie potrzebne. Oznacza to, że klon będzie miał miejsce tylko wtedy, gdy zmienna zostanie zmieniona (kopiowanie przy zapisie).

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 swoich przypadków testowych, możesz łatwo to sobie wyobrazić foreach tworzy pewien rodzaj iteratora z odniesieniem do tablicy. Odwołanie działa dokładnie tak samo jak zmienna $b w moim przykładzie. Jednak iterator wraz z odnośnikiem na żywo tylko w pętli, a następnie są odrzucane. Teraz widzisz, że we wszystkich przypadkach oprócz 3, tablica jest modyfikowana podczas pętli, podczas gdy ten dodatkowy odnośnik jest żywy. To wyzwala klon, a to wyjaśnia, co tu się dzieje!

Oto doskonały artykuł na temat innego skutku ubocznego tego zachowania polegającego na kopiowaniu na piśmie: PHP Ternary Operator: Szybko czy nie?


97
2018-04-07 20:43



wydaje się, że masz rację, zrobiłem przykład, który pokazuje, że: codepad.org/OCjtvu8r jedna różnica od twojego przykładu - nie kopiuje, jeśli zmienisz wartość, tylko jeśli zmieniasz klucze. - zb'
To rzeczywiście wyjaśnia wszystkie powyższe zachowanie i może być ładnie zilustrowane przez wywołanie each() na końcu pierwszego przypadku testowego, gdzie widzimy wskaźnik tablicy oryginalnej tablicy wskazuje drugi element, ponieważ tablica została zmodyfikowana podczas pierwszej iteracji. Wydaje się to również demonstrować foreach przesuwa wskaźnik tablicy przed wykonaniem bloku kodu pętli, czego się nie spodziewałem - pomyślałbym, że zrobi to na końcu. Wielkie dzięki, to ładnie mi to wyjaśnia. - DaveRandom


Niektóre uwagi należy zwrócić uwagę podczas pracy foreach():

za) foreach działa na spodziewana kopia oryginalnej tablicy.     Oznacza to, że foreach () będzie miał przechowywanie danych SHARED do lub do momentu, gdy a prospected copy jest     nie utworzony foreach Uwagi / Komentarze użytkowników.

b) Co wyzwala a spodziewana kopia?     Prospektywna kopia jest tworzona na podstawie polityki copy-on-writeto znaczy, kiedykolwiek     tablica przekazywana do foreach () jest zmieniana, tworzony jest klon oryginalnej tablicy.

c) Oryginalna tablica i iterator foreach () będą miały DISTINCT SENTINEL VARIABLES, to znaczy jeden dla oryginalnej tablicy i inny dla foreach; zobacz kod testowy poniżej. SPL , Iteratory, i Array Iterator.

Pytanie dotyczące przepełnienia stosu Jak upewnić się, że wartość jest resetowany w pętli foreach w PHP? odnosi się do przypadków (3,4,5) twojego pytania.

Poniższy przykład pokazuje, że każde () i reset () NIE wpływają 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/>";

Wydajność:

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

34
2018-04-07 21:03



Twoja odpowiedź nie jest całkiem poprawna. foreach działa na potencjalnej kopii tablicy, ale nie tworzy rzeczywistej kopii, chyba że jest potrzebna. - linepogl
czy chciałbyś pokazać, jak i kiedy ta potencjalna kopia jest tworzona przez kod? Mój kod to demonstruje foreach kopiuje tablicę w 100% przypadków. Chciałbym wiedzieć. Dzięki za komentarze - sakhunzai
Kopiowanie tablicy kosztuje bardzo dużo. Spróbuj zliczyć czas potrzebny do iterowania tablicy przy użyciu 100000 elementów for lub foreach. Nie zauważysz żadnej znaczącej różnicy między nimi dwoma, ponieważ faktyczna kopia nie ma miejsca. - linepogl
Wtedy mógłbym założyć, że tak jest SHARED data storage zastrzeżone do lub chyba copy-on-write , ale (z mojego fragmentu kodu) jest oczywiste, że zawsze będzie DWÓJ zestaw SENTINEL variables jeden dla original array i inne dla foreach. Dzięki, że ma to sens - sakhunzai
"perspektywa"? Czy masz na myśli "chroniony"? - Peter Mortensen


UWAGA DLA PHP 7

Aby zaktualizować tę odpowiedź, ponieważ zyskała ona pewną popularność: Ta odpowiedź nie obowiązuje już od PHP 7. Jak wyjaśniono w "Wsteczne niekompatybilne zmiany", 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 pętla formuje się nad tablicą podaną przez wyrażenie tablicowe. Na każdym   iteracja, wartość bieżącego elementu jest przypisywana do wartości $ i   wewnętrzny wskaźnik tablicy jest zaawansowany o jeden (tak jak w następnym   iteracja, będziesz patrzeć na następny element).

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

W twoim drugim przykładzie zaczynasz od dwóch elementów, a pętla foreach nie znajduje się na ostatnim elemencie, więc ocenia tablicę w następnej iteracji iw ten sposób zdaje sobie sprawę, że istnieje nowy element w tablicy.

Wierzę, że to wszystko jest konsekwencją Przy każdej iteracji część wyjaśnienia w dokumentacji, co prawdopodobnie oznacza, że foreach wykonuje całą logikę zanim wywoła kod {}.

Przypadek testowy

Jeśli uruchomisz to:

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

Otrzymasz to wyjście:

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

Co oznacza, że ​​zaakceptował modyfikację i przeszedł ją, ponieważ został zmodyfikowany "na czas". 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);
?>

Dostaniesz:

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

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

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


22
2018-04-15 08:46



Czy przeczytałeś resztę odpowiedzi? To ma doskonały sens, że foreach decyduje, czy zapętli się jeszcze raz przed nawet uruchamia kod w nim. - Damir Kasipovic
Nie, tablica jest modyfikowana, ale "za późno", ponieważ foreach już "myśli", że jest na ostatnim elemencie (który jest na początku iteracji) i nie będzie już pętli. Gdzie w drugim przykładzie nie jest to ostatni element na początku iteracji i ocenia ponownie na początku następnej iteracji. Próbuję przygotować przypadek testowy. - Damir Kasipovic
@AlmaDo Spójrz na lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Zawsze jest ustawiony na następny wskaźnik, gdy jest iterowany. Tak więc, gdy dojdzie do ostatniej iteracji, będzie oznaczony jako zakończony (poprzez NULL wskaźnik). Kiedy dodasz klucz w ostatniej iteracji, foreach tego nie zauważy. - bwoebi
@DKasipovic no. Nie ma kompletne i jasne wyjaśnienie tam (przynajmniej na razie - może być nie tak) - Alma Do
Właściwie wygląda na to, że @AlmaDo ma wadę w rozumieniu własnej logiki ... Twoja odpowiedź jest w porządku. - bwoebi


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

W każdej iteracji wartość bieżącego elementu jest przypisywana do $ v i wewnętrznego
  wskaźnik tablicy jest zaawansowany o jeden (więc przy kolejnej iteracji będziesz patrzył na następny element).

Tak jak na twój pierwszy przykład:

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

$array mieć tylko jeden element, tak jak na wykonanie foreach, 1 przypisać do $vi nie ma żadnego innego elementu do poruszania wskaźnikiem

Ale w twoim drugim przykładzie:

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

$array mają dwa elementy, więc teraz tablica $ oceń indeksy zerowe i przesuń wskaźnik o jeden. Do pierwszej iteracji pętli dodano $array['baz']=3; jak przejść przez odniesienie.


8
2018-04-15 09:32





Świetne pytanie, ponieważ wielu programistów, nawet doświadczonych, jest zdezorientowanych przez sposób, w jaki PHP obsługuje tablice w pętlach foreach. W standardowej pętli foreach PHP tworzy kopię tablicy, która jest używana w pętli. Kopia jest usuwana natychmiast po zakończeniu pętli. Jest to przejrzyste w działaniu prostej pętli foreach. Na przykład:

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

To daje wynik:

apple
banana
coconut

Tak więc kopia jest tworzona, ale deweloper jej nie zauważa, ponieważ oryginalna tablica nie jest odwoływana w pętli lub po zakończeniu pętli. Jednak przy próbie modyfikacji elementów w pętli, po ich zakończeniu są one niezmodyfikowane:

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

print_r($set);

To daje wynik:

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

Wszelkie zmiany z oryginału nie mogą być zauważone, w rzeczywistości nie ma żadnych zmian w stosunku do oryginału, nawet jeśli wyraźnie przypisano wartość do $ item. Dzieje się tak dlatego, że działasz na pozycji $, która pojawia się w kopii zestawu $, który jest przetwarzany. Możesz to zmienić, przechwytując $ item przez referencję, jak na przykład:

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

To daje wynik:

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

Jest więc oczywiste i możliwe do zaobserwowania, gdy element $ item działa na zasadzie odsyłacza, zmiany dokonane w pozycji $ są dokonywane na elementach oryginalnego zestawu $. Użycie $ item by reference zapobiega także tworzeniu kopii tablicy przez PHP. 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 daje wynik:

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 w pętli użyto $ set, PHP dodał 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 się tylko 3 razy i za każdym razem dołącza inną wartość do końca oryginalnego zestawu $ set, pozostawiając oryginalny zestaw $ z 6 elementami, ale nigdy nie wprowadzając nieskończonej pętli.

Co jednak, jeśli użyliśmy pozycji $ za referencję, jak już 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);

Wyniki w nieskończonej pętli. Zauważ, że w rzeczywistości jest to nieskończona pętla, musisz albo samemu zabić skrypt, albo poczekać, aż systemowi zabraknie pamięci. Dodałem następujący wiersz do mojego skryptu, więc PHP bardzo szybko zabraknie pamięci, sugeruję, aby zrobić to samo, jeśli zamierzasz uruchomić te nieskończone testy pętli:

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

Tak więc w tym poprzednim przykładzie z nieskończoną pętlą, widzimy powód, dla którego napisano PHP, aby utworzyć kopię tablicy do pętli. Kiedy kopia jest tworzona i używana tylko przez strukturę konstrukcji pętli, tablica pozostaje statyczna w trakcie wykonywania pętli, więc nigdy nie napotkasz problemów.


5
2018-04-21 08:44





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

W pętli foreach pierwszą rzeczą, jaką robi php, jest utworzenie kopii tablicy, która ma być iterowana. PHP następnie iteruje nad tym nowym copy tablicy, a nie oryginalnej. Zostało to wykazane 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 na używanie iterated values as a reference to the original array value także. Zostało to wykazane 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: To nie pozwala original array indexes do wykorzystania jako references.

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


4
2017-11-13 14:08



Object public variables jest niewłaściwy lub w najlepszym przypadku mylący. Nie możesz użyć obiektu w tablicy bez poprawnego interfejsu (np. Traversible) i kiedy to robisz foreach((array)$obj ... w rzeczywistości pracujesz z prostą tablicą, a nie obiektem. - Christian