Pozwólcie, że poprzedzę to stwierdzeniem, że wiem, co to foreach
jest, robi i jak z niego korzystać. To pytanie dotyczy tego, jak działa pod maską, i nie chcę żadnych odpowiedzi w stylu „w ten sposób zapętlasz tablicę za pomocą 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 ostatnio wdałem się w dyskusję na ten temat i po niewielkich eksperymentach okazało się, że nie jest to w 100% prawda.
Pokażę, 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);
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 trwać wiecznie, ponieważ w trakcie pętli ciągle popychamy elementy do tablicy. Ale dla pewności tak jest:
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 nasze początkowe wnioski, pracujemy z kopią tablicy źródłowej podczas pętli, w przeciwnym razie widzielibyśmy zmodyfikowane wartości podczas pętli. Ale...
Jeśli zajrzymy do instrukcji , znajdziemy następujące stwierdzenie:
Kiedy foreach zaczyna działać, wewnętrzny wskaźnik tablicy jest automatycznie resetowany do pierwszego elementu tablicy.
Racja ... wydaje się to sugerować, że foreach
zależy od wskaźnika tablicy tablicy źródłowej. Ale właśnie udowodniliśmy, że nie pracujemy z tablicą źródłową , prawda? Cóż, nie do końca.
// 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 - pokazuje to fakt, że wskaźnik znajduje się na końcu tablicy na końcu pętli. Tyle że to nie może być prawda - gdyby tak było, to przypadek testowy 1 zapętlałby się na zawsze.
Podręcznik PHP stwierdza również:
Ponieważ foreach opiera się na wskaźniku tablicy wewnętrznej, zmiana go w pętli może prowadzić do nieoczekiwanego zachowania.
Cóż, dowiedzmy się, czym jest to „nieoczekiwane zachowanie” (technicznie każde zachowanie jest nieoczekiwane, ponieważ nie wiem już, czego się spodziewać).
foreach ($array as $key => $item) {
echo "$item\n";
each($array);
}
/* Output: 1 2 3 4 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ę potwierdzać teorię „kopii źródła”.
Pytanie
Co tu się dzieje? Moje C-fu nie jest wystarczająco dobre, abym mógł 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.
Wydaje mi się, że foreach
działa z kopią tablicy, ale ustawia wskaźnik tablicy tablicy źródłowej na końcu tablicy po pętli.
- Czy to prawda i cała historia?
- Jeśli nie, co tak naprawdę robi?
- Czy jest jakaś sytuacja, w której użycie funkcji, które dostosowują wskaźnik tablicy (
each()
,reset()
i in.) Podczas a,foreach
może wpłynąć na wynik pętli?
foreach ($array as &$value)
) - PHP musi znać aktualną pozycję w oryginalnej tablicy, nawet jeśli faktycznie iteruje po kopii.Odpowiedzi:
foreach
obsługuje iterację trzech różnych rodzajów wartości:Traversable
przedmiotyPoniżej postaram się dokładnie wyjaśnić, w jaki sposób iteracja działa w różnych przypadkach. Zdecydowanie najprostszym przypadkiem są
Traversable
obiekty, ponieważ dla nichforeach
jest to zasadniczo tylko cukier składniowy dla kodu wzdłuż tych linii:W przypadku klas wewnętrznych można uniknąć rzeczywistych wywołań metod, używając wewnętrznego interfejsu API, który zasadniczo odzwierciedla
Iterator
interfejs na poziomie C.Iteracja tablic i prostych 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 odpowiada kolejności wstawiania, o ile nie użyłeś czegoś takiego
sort
). Przeciwstawia się to powtarzaniu przez naturalną kolejność klawiszy (jak często działają listy w innych językach) lub brak zdefiniowanej kolejności (jak często działają słowniki w innych językach).To samo dotyczy również obiektów, ponieważ właściwości obiektu można postrzegać jako kolejną (uporządkowaną) nazwę właściwości mapowania słownika na ich wartości, a także pewną obsługę 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 zaczniesz iterować nad obiektem, normalnie używana spakowana reprezentacja zostanie przekonwertowana na prawdziwy słownik. W tym momencie iteracja prostych obiektów staje się bardzo podobna do iteracji tablic (dlatego nie omawiam tutaj dużo iteracji prostych obiektów).
Na razie w porządku. Iterowanie po słowniku nie może być zbyt trudne, prawda? Problemy zaczynają się, gdy zdajesz sobie sprawę, że tablica / obiekt może się zmieniać podczas iteracji. Może się to zdarzyć na wiele sposobów:
foreach ($arr as &$v)
wówczas$arr
zostanie zmieniona w referencję i możesz ją zmienić podczas iteracji.$ref =& $arr; foreach ($ref as $v)
Problem z zezwoleniem na modyfikacje podczas iteracji polega na tym, że element, na którym jesteś aktualnie, jest usuwany. Załóżmy, że używasz wskaźnika, aby śledzić, w którym elemencie tablicy aktualnie się znajdujesz. Jeśli ten element zostanie teraz uwolniony, pozostanie z wiszącym wskaźnikiem (zwykle skutkującym awarią).
Istnieją różne sposoby rozwiązania tego problemu. PHP 5 i PHP 7 różnią się znacznie pod tym względem i opiszę oba zachowania poniżej. Podsumowując, podejście PHP 5 było raczej głupie i prowadziło do różnego rodzaju dziwnych problemów na krawędziach, podczas gdy podejście bardziej zaangażowane w PHP 7 daje bardziej przewidywalne i spójne zachowanie.
Na koniec należy zauważyć, że PHP wykorzystuje liczenie referencji i kopiowanie przy zapisie do zarządzania pamięcią. Oznacza to, że jeśli „skopiujesz” wartość, w rzeczywistości po prostu ponownie użyjesz starej wartości i zwiększysz jej liczbę referencyjną (przeliczanie). Dopiero po wykonaniu jakiejś modyfikacji zostanie wykonana prawdziwa kopia (zwana „duplikacją”). Zobacz, w jaki sposób jesteś okłamywany, aby uzyskać bardziej obszerne wprowadzenie na ten temat.
PHP 5
Wskaźnik tablicy wewnętrznej i HashPointer
Tablice w PHP 5 mają jeden dedykowany „wewnętrzny wskaźnik tablicy” (IAP), który poprawnie obsługuje modyfikacje: Ilekroć element zostanie usunięty, będzie sprawdzane, czy IAP wskazuje na ten element. Jeśli tak, to zamiast tego przechodzi do następnego elementu.
Chociaż
foreach
korzysta z IAP, istnieje dodatkowa komplikacja: istnieje tylko jeden IAP, ale jedna tablica może być częścią wieluforeach
pętli:Aby obsłużyć dwie równoczesne pętle za pomocą tylko jednego wewnętrznego wskaźnika tablicy,
foreach
wykonaj następujące shenanigany: Przed wykonaniem korpusu pętliforeach
utworzy kopię zapasową wskaźnika do bieżącego elementu i jego skrótu w foreachHashPointer
. Po uruchomieniu treści pętli IAP zostanie przywrócony do tego elementu, jeśli nadal istnieje. Jeśli jednak element zostanie usunięty, użyjemy go wszędzie tam, gdzie jest obecnie IAP. Ten schemat działa w pewnym sensie, ale można się z niego wydostać wiele dziwnych zachowań, niektóre z nich przedstawię poniżej.Powielanie tablic
IAP jest widoczną cechą tablicy (widoczną przez
current
rodzinę funkcji), ponieważ takie zmiany w IAP liczą się jako modyfikacje w ramach semantyki kopiowania przy zapisie. To niestety oznacza, żeforeach
w wielu przypadkach jest zmuszony do zduplikowania tablicy, nad którą się iteruje. Dokładne warunki to:refcount
wynosi 1, to tablica nie jest współdzielona i możemy ją modyfikować bezpośrednio.Jeśli tablica nie jest zduplikowana (is_ref = 0, refcount = 1), wówczas tylko jej wartość
refcount
zostanie zwiększona (*). Dodatkowo, jeśliforeach
zostanie użyte odniesienie, tablica (potencjalnie zduplikowana) zostanie przekształcona w odwołanie.Rozważ ten kod jako przykład, w którym występuje duplikacja:
Tutaj
$arr
będą powielane, aby zapobiec IAP zmienia on$arr
z wyciekającego$outerArr
. Pod względem powyższych warunków tablica nie jest odwołaniem (is_ref = 0) i jest używana w dwóch miejscach (refcount = 2). To wymaganie jest niefortunne i jest artefaktem nieoptymalnej implementacji (tutaj nie ma obaw o modyfikację podczas iteracji, więc tak naprawdę nie musimy używać IAP).(*) Zwiększanie wartości
refcount
tutaj brzmi nieszkodliwie, ale narusza semantykę kopiowania przy zapisie (COW): Oznacza to, że będziemy modyfikować IAP tablicy refcount = 2, podczas gdy COW dyktuje, że modyfikacje mogą być wykonywane tylko przy refcount = 1 wartości. Naruszenie to powoduje zmianę zachowania widoczną dla użytkownika (podczas gdy COW jest zwykle przezroczysty), ponieważ zmiana IAP w tablicy iterowanej będzie obserwowalna - ale tylko do pierwszej modyfikacji innej niż IAP w tablicy. Zamiast tego trzy „prawidłowe” opcje byłyby a) zawsze powielane, b) nie zwiększały,refcount
a tym samym pozwalały na dowolną modyfikację tablicy iterowanej w pętli lub c) w ogóle nie korzystały z IAP (PHP 7 rozwiązanie).Pozycja awansu pozycji
Jest jeszcze jeden szczegół implementacji, o którym musisz wiedzieć, aby poprawnie zrozumieć poniższe przykłady kodu. „Normalny” sposób zapętlania struktury danych wyglądałby w pseudokodzie mniej więcej tak:
Jednak
foreach
będąc specjalnym płatkiem śniegu, robi rzeczy nieco inaczej:Mianowicie wskaźnik tablicy jest już przesuwany do przodu przed uruchomieniem treści pętli. Oznacza to, że gdy ciało pętli działa na elemencie
$i
, IAP jest już na elemencie$i+1
. To jest powód, dlaczego próbki kodu pokazano modyfikację podczas iteracji będzie zawsze obok elementu, niż ten obecny.unset
Przykłady: Twoje przypadki testowe
Trzy aspekty opisane powyżej powinny dać ci w większości pełne wrażenie osobliwości
foreach
implementacji i możemy przejść do omówienia niektórych przykładów.Zachowanie przypadków testowych jest w tym miejscu łatwe do wyjaśnienia:
W przypadkach testowych 1 i 2
$array
zaczyna się od refcount = 1, więc nie będzie duplikowane przezforeach
: Tylko wartośćrefcount
jest zwiększana. Kiedy ciało pętli następnie zmodyfikuje tablicę (która ma w tym momencie refcount = 2), w tym momencie nastąpi duplikacja. Foreach będzie kontynuował pracę nad niezmodyfikowaną kopią$array
.W przypadku testowym 3 tablica po raz kolejny nie jest duplikowana, a zatem
foreach
będzie modyfikować IAP$array
zmiennej. Pod koniec iteracji IAP ma wartość NULL (co oznacza, że wykonano iterację), coeach
oznacza powrótfalse
.W przypadku testów 4 i 5, jak
each
ireset
są funkcjami przez odwołanie.$array
Marefcount=2
, gdy jest przekazywana do nich, więc to musi być powielane. W związku z tymforeach
ponownie będzie pracował na osobnej tablicy.Przykłady: Skutki
current
w foreachDobrym sposobem na pokazanie różnych zachowań związanych z powielaniem jest obserwowanie zachowania
current()
funkcji wewnątrzforeach
pętli. Rozważ ten przykład:Tutaj powinieneś wiedzieć, że
current()
jest to funkcja by-ref (właściwie: prefer-ref), nawet jeśli nie modyfikuje tablicy. Musi być po to, aby dobrze grać ze wszystkimi innymi funkcjami, takimi jak te,next
które są by-ref. Przekazywanie przez referencje oznacza, że tablica musi być oddzielona, a zatem$array
iforeach-array
będzie inna. Powód, dla którego dostajesz2
zamiast,1
jest również wspomniany powyżej:foreach
przesuwa wskaźnik tablicy przed uruchomieniem kodu użytkownika, a nie po nim. Więc nawet jeśli kod znajduje się na pierwszym elemencie,foreach
wskaźnik przesunął się już do drugiego.Teraz spróbujmy małej modyfikacji:
Tutaj mamy przypadek is_ref = 1, więc tablica nie jest kopiowana (tak jak powyżej). Ale teraz, gdy jest to odwołanie, tablica nie musi już być duplikowana podczas przekazywania do funkcji by-ref
current()
. W ten sposóbcurrent()
iforeach
pracy w tym samym układzie. Nadal jednak widzisz zachowanie indywidualne, ze względu na sposóbforeach
przesuwania wskaźnika.Takie samo zachowanie uzyskuje się podczas iteracji według odwołania:
Ważną częścią jest to, że foreach wykona
$array
is_ref = 1, gdy jest iterowane przez odniesienie, więc w zasadzie masz taką samą sytuację jak powyżej.Kolejna mała odmiana, tym razem przypiszemy tablicę do innej zmiennej:
Tutaj liczba zwrotna
$array
wynosi 2, gdy pętla jest uruchamiana, więc tym razem musimy faktycznie wykonać kopię z góry. Zatem$array
tablica używana przez foreach będzie całkowicie oddzielna od samego początku. Dlatego uzyskujesz 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 podczas iteracji jest źródłem wszystkich naszych problemów z wyprzedzeniem, dlatego warto rozważyć kilka przykładów tego przypadku.
Rozważ te zagnieżdżone pętle w tej samej tablicy (gdzie iteracja by-ref jest używana, aby upewnić się, że naprawdę jest taka sama):
Oczekiwano tu części, której
(1, 2)
brakuje w danych wyjściowych, ponieważ element1
został usunięty. Prawdopodobnie nieoczekiwane jest to, że zewnętrzna pętla zatrzymuje się po pierwszym elemencie. Dlaczego?Powodem tego jest włamanie do pętli zagnieżdżonej opisane powyżej: Przed uruchomieniem treści pętli bieżąca pozycja IAP i skrót są zapisywane w pliku
HashPointer
. Po korpusie pętli zostanie przywrócony, ale tylko jeśli element nadal istnieje, w przeciwnym razie zostanie użyta bieżąca pozycja IAP (cokolwiek by to nie było). W powyższym przykładzie jest to dokładnie taki 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ę!Inną konsekwencją
HashPointer
mechanizmu tworzenia kopii zapasowych i przywracania jest to, że zmiany w IAP za pośrednictwemreset()
itp. Zwykle nie mają wpływuforeach
. Na przykład następujący kod jest wykonywany tak, jakby wreset()
ogóle nie był obecny:Powodem jest to, że chociaż
reset()
tymczasowo modyfikuje IAP, zostanie on przywrócony do bieżącego elementu foreach po treści pętli. Aby wymusićreset()
efekt na pętli, musisz dodatkowo usunąć bieżący element, aby mechanizm tworzenia kopii zapasowych / przywracania nie działał:Ale te przykłady są nadal rozsądne. Prawdziwa zabawa zaczyna się, jeśli pamiętasz, że
HashPointer
przywracanie używa wskaźnika do elementu i jego skrótu, aby ustalić, czy nadal istnieje. Ale: Hashe mają kolizje, a wskaźniki mogą być ponownie użyte! Oznacza to, że przy starannym doborze kluczy tablicowych możemy sprawićforeach
, że usunięty element nadal istnieje, więc przeskoczy bezpośrednio do niego. Przykład:Tutaj normalnie powinniśmy oczekiwać wyników
1, 1, 3, 4
zgodnie z poprzednimi zasadami. To, co się dzieje,'FYFY'
ma taki sam skrót jak usunięty element'EzFY'
, a alokator ponownie wykorzystuje tę samą lokalizację pamięci do przechowywania elementu. Foreach kończy się więc skokiem bezpośrednio do nowo wstawionego elementu, tym samym skracając pętlę.Zastępowanie iterowanej jednostki podczas pętli
Ostatni nieparzysty przypadek, o którym chciałbym wspomnieć, to to, że PHP pozwala na zastąpienie iterowanej jednostki podczas pętli. Możesz więc rozpocząć iterację na jednej tablicy, a następnie zastąpić ją inną w połowie. Lub rozpocznij iterację tablicy, a następnie zamień ją na obiekt:
Jak widać w tym przypadku, PHP po prostu zacznie iterację od drugiej jednostki, gdy nastąpi podstawienie.
PHP 7
Iteratory haszujące
Jeśli nadal pamiętasz, głównym problemem związanym z iteracją tablicy było to, jak radzić sobie z usuwaniem elementów w trakcie iteracji. W tym celu PHP 5 użył pojedynczego wewnętrznego wskaźnika tablicy (IAP), który był nieco nieoptymalny, ponieważ jeden wskaźnik tablicy musiał zostać rozciągnięty, aby obsługiwać wiele jednoczesnych pętli foreach i interakcji z
reset()
itp.PHP 7 stosuje inne podejście, mianowicie obsługuje tworzenie dowolnej liczby zewnętrznych, bezpiecznych iteratorów z mieszaniem. Te iteratory muszą być zarejestrowane w tablicy, od tego momentu mają taką samą semantykę jak IAP: Jeśli element tablicy zostanie usunięty, wszystkie iteratory hashtable wskazujące na ten element zostaną przeniesione do następnego elementu.
Oznacza to, że
foreach
nie będą używać IAP w ogóle .foreach
Pętla będzie absolutnie żadnego wpływu na wynikachcurrent()
itd, a jego własne zachowanie nigdy nie będzie pod wpływem funkcji, takich jakreset()
etc.Powielanie tablic
Kolejna ważna zmiana między PHP 5 a PHP 7 dotyczy powielania tablic. Teraz, gdy IAP nie jest już używany, iteracja tablic według wartości spowoduje tylko
refcount
zwiększenie (zamiast duplikowania tablicy) we wszystkich przypadkach. Jeśli tablica zostanie zmodyfikowana podczasforeach
pętli, w tym momencie nastąpi duplikacja (zgodnie z kopiowaniem przy zapisie) iforeach
nadal będzie działać na starej tablicy.W większości przypadków zmiana ta jest przejrzysta i nie ma innego efektu niż lepsza wydajność. Istnieje jednak jeden przypadek, w którym powoduje to inne zachowanie, a mianowicie przypadek, w którym tablica była wcześniej odniesieniem:
Poprzednio iteracja według wartości tablic referencyjnych była przypadkiem szczególnym. W takim przypadku nie wystąpiło powielanie, więc wszystkie modyfikacje tablicy podczas iteracji zostaną odzwierciedlone przez pętlę. W PHP 7 tego szczególnego przypadku nie ma: iteracja według wartości tablicy zawsze będzie działać na oryginalnych elementach, pomijając wszelkie modyfikacje podczas pętli.
To oczywiście nie dotyczy iteracji przez odniesienie. W przypadku iteracji według odwołania wszystkie modyfikacje zostaną odzwierciedlone przez pętlę. Co ciekawe, to samo dotyczy iteracji według wartości zwykłych obiektów:
Odzwierciedla to semantykę uchwytów obiektów (tzn. Zachowują się one 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 tablic według wartości zawsze działa na oryginalne elementy. (W tym przypadku
refcounting
zachowanie parzystości i duplikacji jest dokładnie takie samo między PHP 5 i PHP 7).Zmiany w przypadku testowym 3:
Foreach
nie używa już IAP, więceach()
pętla nie ma na niego wpływu. Będzie miał tę samą moc przed i po.Przypadki testowe 4 i 5 pozostają takie same:
each()
ireset()
powielą tablicę przed zmianą IAP,foreach
nadal wykorzystując tablicę oryginalną. (Nie żeby zmiana IAP miała znaczenie, nawet gdyby tablica była współdzielona).Drugi zestaw przykładów związany był z zachowaniem się
current()
w różnychreference/refcounting
konfiguracjach. Nie ma to już sensu, ponieważcurrent()
pętla całkowicie na nią nie ma wpływu, więc jego wartość zwrotna zawsze pozostaje taka sama.Dostajemy jednak kilka interesujących zmian przy rozważaniu modyfikacji podczas iteracji. Mam nadzieję, że odkryjesz, że nowe zachowanie jest zdrowsze. Pierwszy przykład:
Jak widać, zewnętrzna pętla nie jest już przerywana po pierwszej iteracji. Powodem jest to, że obie pętle mają teraz całkowicie oddzielne iteratory z mieszaniem, i nie ma już zanieczyszczenia krzyżowego obu pętli poprzez wspólny IAP.
Innym dziwnym przypadkiem krawędzi, który został teraz naprawiony, jest dziwny efekt, który uzyskujesz, gdy usuwasz i dodajesz elementy, które mają taki sam skrót:
Wcześniej mechanizm przywracania HashPointer przeskakiwał w prawo do nowego elementu, ponieważ „wyglądał” tak, jakby był taki sam jak usunięty element (z powodu kolizji skrótu i wskaźnika). Ponieważ do niczego już nie polegamy na haszu elementu, nie stanowi to już problemu.
źródło
$foo = $array
przed pętlą;)Bucket
s jest częścią podwójnie połączonej listy kolizji kratki, a także część podwójnie połączonej listy do celu;)iterate($outerArr);
a nieiterate($arr);
gdzieś.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 tablice PHP ze względu na semantykę operatora przypisania.
Operator przypisania tablic w PHP działa bardziej jak leniwy klon. Przypisanie jednej zmiennej do drugiej zawierającej tablicę spowoduje klonowanie 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 nastąpi tylko wtedy, gdy jedna ze zmiennych zostanie zmodyfikowana (kopiowanie przy zapisie).
Oto przykład:
Wracając do przypadków testowych, możesz łatwo wyobrazić sobie, że
foreach
tworzy pewnego rodzaju iterator z odniesieniem do tablicy. To odwołanie działa dokładnie tak jak zmienna$b
w moim przykładzie. Jednak iterator wraz z referencją są aktywne 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 aktywne. To uruchamia klon, a to wyjaśnia, co się tutaj dzieje!Oto doskonały artykuł na temat innego efektu ubocznego tego zachowania podczas kopiowania: Operator trójskładnikowy PHP: szybki czy nie?
źródło
each()
na końcu pierwszego przypadku testowego, gdzie widzimy, że wskaźnik tablicy oryginalnej tablicy wskazuje na drugi element, ponieważ tablica została zmodyfikowana podczas pierwsza iteracja. Wydaje się to również pokazywać, żeforeach
przesuwa wskaźnik tablicy przed wykonaniem bloku kodu pętli, czego się nie spodziewałem - pomyślałem, że zrobi to na końcu. Wielkie dzięki, to mi ładnie to wyjaśnia.Kilka punktów, na które należy zwrócić uwagę podczas pracy z
foreach()
:a)
foreach
działa na potencjalnej kopii oryginalnej tablicy. Oznacza toforeach()
, że będzie mieć UDOSTĘPNIONE przechowywanie danych do momentu, aż nieprospected copy
zostanie utworzone żadne powiadomienie / uwagi użytkownika .b) Co uruchamia potencjalną kopię ? Spodziewana kopia jest tworzona na podstawie zasady
copy-on-write
, tzn. Za każdym razem, gdyforeach()
zmienia się przekazywana tablica, tworzony jest klon oryginalnej tablicy.c) Oryginalna tablica i
foreach()
iterator będą miałyDISTINCT SENTINEL VARIABLES
jedną dla oryginalnej tablicy i drugą dlaforeach
; patrz kod testu poniżej. SPL , Iteratory i Array Iterator .Pytanie o przepełnieniu 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 każdy () i reset () nie ma wpływu
SENTINEL
zmiennych(for example, the current index variable)
naforeach()
iteracyjnej.Wynik:
źródło
foreach
działa na potencjalnej kopii tablicy, ale nie tworzy rzeczywistej kopii, chyba że jest potrzebna.foreach
kopiuje tablicę w 100% przypadków. Chcę wiedzieć. Dziękuję za komentarzefor
alboforeach
. Nie zobaczysz żadnej znaczącej różnicy między nimi dwoma, ponieważ faktyczna kopia nie ma miejsca.SHARED data storage
zarezerwowane, dopóki niecopy-on-write
, ale (z mojego fragmentu kodu) jest oczywiste, że zawsze będzie DWA zestawSENTINEL variables
jednego dlaoriginal array
drugiego i drugiego dlaforeach
. Dzięki, że to ma sensUWAGA DLA PHP 7
Aby zaktualizować tę odpowiedź, ponieważ zyskała ona popularność: Ta odpowiedź nie dotyczy już PHP 7. Jak wyjaśniono w „ Wstecznych niezgodnych zmianach ”, w PHP 7 foreach działa na kopię tablicy, więc wszelkie zmiany na samej tablicy nie są odzwierciedlone w pętli foreach. Więcej szczegółów pod linkiem.
Objaśnienie (cytat z php.net ):
Tak więc w pierwszym przykładzie masz tylko jeden element w tablicy, a kiedy wskaźnik zostanie przesunięty, następny element nie istnieje, więc po dodaniu nowego elementu foreach kończy się, ponieważ już „zdecydował”, że jest to ostatni element.
W drugim przykładzie zaczynasz od dwóch elementów, a pętla foreach nie znajduje się na ostatnim elemencie, więc ocenia tablicę podczas następnej iteracji i w ten sposób zdaje sobie sprawę, że w tablicy jest nowy element.
Uważam, że jest to konsekwencja części wyjaśnienia w każdej iteracji w dokumentacji, co prawdopodobnie oznacza, że
foreach
robi całą logikę przed wywołaniem kodu{}
.Przypadek testowy
Jeśli uruchomisz to:
Otrzymasz ten wynik:
Co oznacza, że zaakceptował modyfikację i przeszedł przez nią, ponieważ została zmodyfikowana „na czas”. Ale jeśli to zrobisz:
Dostaniesz:
Co oznacza, że tablica została zmodyfikowana, ale ponieważ zmodyfikowaliśmy ją, gdy
foreach
znajdował się już przy ostatnim elemencie tablicy, „postanowił”, że nie będzie już zapętlać, i mimo że dodaliśmy nowy element, dodaliśmy go „za późno” i to nie został zapętlony.Szczegółowe wyjaśnienie można znaleźć na stronie Jak faktycznie działa „foreach” PHP? co wyjaśnia elementy wewnętrzne tego zachowania.
źródło
Zgodnie z dokumentacją dostarczoną w podręczniku PHP.
Tak jak w twoim pierwszym przykładzie:
$array
mają tylko jeden element, więc zgodnie z wykonaniem foreach, 1 przypisaj do$v
i nie ma on żadnego innego elementu do przesunięcia wskaźnikaAle w twoim drugim przykładzie:
$array
mają dwa elementy, więc teraz $ tablica ocenia wskaźniki zerowe i przesuwa wskaźnik o jeden. Do pierwszej iteracji pętli dodawanej$array['baz']=3;
jako referencja.źródło
Świetne pytanie, ponieważ wielu programistów, nawet 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 przejrzyste w działaniu prostej pętli foreach. Na przykład:
To daje:
Tak więc kopia jest tworzona, ale programista nie zauważa, ponieważ do oryginalnej tablicy nie ma odniesienia w pętli ani po jej zakończeniu. Jednak podczas próby modyfikacji elementów w pętli okazuje się, że nie są one modyfikowane po zakończeniu:
To daje:
Wszelkie zmiany w stosunku do oryginału nie mogą być powiadomieniami, w rzeczywistości nie ma żadnych zmian w stosunku do oryginału, nawet jeśli wyraźnie przypisałeś wartość do przedmiotu $. Wynika to z faktu, że operujesz na $ item, ponieważ pojawia się on w kopii przetwarzanego zestawu $ set. Możesz to zmienić, chwytając $ item przez odniesienie, w ten sposób:
To daje:
Jest więc oczywiste i możliwe do zaobserwowania, że gdy $ item działa na podstawie odwołania, zmiany wprowadzone w $ item są dokonywane w elementach oryginalnego zestawu $. Użycie $ item przez odniesienie również zapobiega tworzeniu kopii tablicy przez PHP. Aby to przetestować, najpierw pokażemy szybki skrypt demonstrujący kopię:
To daje:
Jak pokazano w przykładzie, PHP skopiował $ set i użył go do zapętlenia, ale kiedy $ set został użyty w pętli, 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 dodaje kolejną wartość na końcu oryginalnego zestawu $, pozostawiając oryginalny zestaw $ z 6 elementami, ale nigdy nie wchodzi w nieskończoną pętlę.
Co jednak, jeśli użyliśmy $ item przez odniesienie, jak wspomniałem wcześniej? Pojedynczy znak dodany do powyższego testu:
Wynikiem jest nieskończona pętla. Zauważ, że tak naprawdę jest to nieskończona pętla, musisz albo zabić skrypt samodzielnie, albo poczekać, aż zabraknie pamięci w twoim systemie operacyjnym. Dodałem następujący wiersz do mojego skryptu, aby PHP szybko zabrakło pamięci, sugeruję zrobić to samo, jeśli zamierzasz uruchomić te testy nieskończonej pętli:
Tak więc w poprzednim przykładzie z nieskończoną pętlą widzimy powód, dla którego napisano PHP, aby utworzyć kopię tablicy do zapętlenia. Kiedy kopia jest tworzona i używana tylko przez samą strukturę konstrukcji pętli, tablica pozostaje statyczna przez cały czas wykonywania pętli, więc nigdy nie napotkasz problemów.
źródło
Pętla foreach PHP może być używana z
Indexed arrays
,Associative arrays
iObject public variables
.W pętli foreach pierwszą rzeczą, którą robi php, jest to, że tworzy kopię tablicy, która ma być iterowana. PHP następnie iteruje tę nową
copy
tablicę zamiast oryginalnej. Pokazano to w poniższym przykładzie:Poza tym php pozwala również na użycie
iterated values as a reference to the original array value
. Pokazano to poniżej:Uwaga: Nie
original array indexes
można go używać jakoreferences
.Źródło: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples
źródło
Object public variables
jest zły lub w najlepszym przypadku wprowadzający w błąd. Nie możesz użyć obiektu w tablicy bez odpowiedniego interfejsu (np. Traversible), a kiedy to robiszforeach((array)$obj ...
, w rzeczywistości pracujesz z prostą tablicą, a nie obiektem.