etc./StackOverFlow

PHP 'foreach'는 실제로 어떻게 작동합니까?

청렴결백한 만능 재주꾼 2021. 12. 2. 00:26
반응형

질문자 :DaveRandom


foreach 가 무엇인지, 무엇을 하는지, 어떻게 사용하는지 알고 있다는 접두어를 붙이겠습니다. 이 질문은 그것이 보닛 아래에서 어떻게 작동하는지에 관한 것이며 " foreach 하여 배열을 반복하는 방법입니다"와 같은 대답을 원하지 않습니다.


오랫동안 나는 foreach 가 어레이 자체와 함께 작동한다고 가정했습니다. 그런 다음 배열의 복사본 과 함께 작동한다는 사실에 대한 많은 참조를 찾았고 이후로 이것이 이야기의 끝이라고 가정했습니다. 그러나 나는 최근에 그 문제에 대해 토론을 했고 약간의 실험 후에 이것이 사실 100% 사실이 아니라는 것을 발견했습니다.

내가 의미하는 바를 보여 드리겠습니다. 다음 테스트 사례의 경우 다음 어레이로 작업합니다.

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

테스트 케이스 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 */

이것은 우리가 소스 어레이로 직접 작업하고 있지 않다는 것을 분명히 보여줍니다. 그렇지 않으면 루프 중에 항목을 어레이에 지속적으로 밀어넣기 때문에 루프가 영원히 계속될 것입니다. 그러나 이것이 사실인지 확인하기 위해:

테스트 케이스 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 */

이것은 우리의 초기 결론을 뒷받침합니다. 우리는 루프 동안 소스 배열의 복사본으로 작업하고 있습니다. 그렇지 않으면 루프 중에 수정된 값을 보게 될 것입니다. 하지만...

설명서를 보면 다음과 같은 문구가 있습니다.

foreach가 처음 실행을 시작하면 내부 배열 포인터가 자동으로 배열의 첫 번째 요소로 재설정됩니다.

foreach 가 소스 배열의 배열 포인터에 의존한다는 것을 암시하는 것 같습니다. 하지만 우리는 소스 배열로 작업하지 않는다는 것을 방금 증명했습니다. 맞습니까? 글쎄요, 완전히는 아닙니다.

테스트 케이스 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) */

따라서 소스 배열로 직접 작업하지 않는다는 사실에도 불구하고 소스 배열 포인터로 직접 작업하고 있습니다. 포인터가 루프 끝에서 배열의 끝에 있다는 사실이 이를 보여줍니다. 이것이 사실일 수 없다는 점을 제외하고 - 만약 그렇다면 테스트 케이스 1 은 영원히 반복될 것입니다.

PHP 매뉴얼에도 다음과 같이 나와 있습니다.

foreach는 내부 배열 포인터에 의존하므로 루프 내에서 이를 변경하면 예기치 않은 동작이 발생할 수 있습니다.

글쎄, 그 "예기치 않은 행동"이 무엇인지 알아봅시다(기술적으로, 더 이상 무엇을 기대해야 할지 모르기 때문에 모든 행동은 예상치 못한 것입니다).

테스트 케이스 4 :

 foreach ($array as $key => $item) { echo "$item\n"; each($array); } /* Output: 1 2 3 4 5 */

테스트 케이스 5 :

 foreach ($array as $key => $item) { echo "$item\n"; reset($array); } /* Output: 1 2 3 4 5 */

... 거기에는 예상치 못한 것이 없었습니다. 사실 "원본 복사" 이론을 지지하는 것 같습니다.


질문

무슨 일이야? 제 C-fu는 단순히 PHP 소스 코드를 보고 적절한 결론을 추출할 수 있을 만큼 충분하지 않습니다. 누군가 그것을 영어로 번역해 주시면 감사하겠습니다.

foreach 는 배열의 복사본 과 함께 작동하지만 소스 배열의 배열 포인터를 루프 뒤 배열의 끝으로 설정하는 것 같습니다.

  • 이것이 정확하고 전체 이야기입니까?
  • 그렇지 않다면 그것은 실제로 무엇을 하고 있습니까?
  • foreach 동안 each() , reset() 등)를 조정하는 함수를 사용하면 루프의 결과에 영향을 미칠 수 있는 상황이 있습니까?


foreach 는 세 가지 다른 종류의 값에 대한 반복을 지원합니다.

다음에서 나는 반복이 다른 경우에 어떻게 작동하는지 정확하게 설명하려고 노력할 것입니다. 지금까지 가장 간단한 경우는 Traversable 객체입니다. 이러한 foreach 는 본질적으로 다음 행에 따른 코드에 대한 구문 설탕일 뿐입니다.

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

내부 클래스의 경우 본질적으로 C 수준 Iterator 인터페이스를 미러링하는 내부 API를 사용하여 실제 메서드 호출을 피할 수 있습니다.

배열과 일반 객체의 반복은 훨씬 더 복잡합니다. 우선, PHP에서 "배열"은 실제로 순서가 지정된 사전이며 이 순서에 따라 순회된다는 점에 유의해야 합니다( sort 와 같은 것을 사용하지 않는 한 삽입 순서와 일치). 이것은 키의 자연스러운 순서(다른 언어의 목록이 자주 작동하는 방식)에 따라 반복하거나 정의된 순서가 전혀 없는(다른 언어의 사전이 자주 작동하는 방식) 반대입니다.

객체 속성은 속성 이름을 해당 값에 매핑하는 다른(순서화된) 사전으로 볼 수 있고 가시성 처리도 포함되므로 객체에도 동일하게 적용됩니다. 대부분의 경우 개체 속성은 실제로 이 다소 비효율적인 방식으로 저장되지 않습니다. 그러나 객체에 대해 반복을 시작하면 일반적으로 사용되는 압축 표현이 실제 사전으로 변환됩니다. 그 시점에서 일반 객체의 반복은 배열의 반복과 매우 유사해집니다(이 때문에 여기에서 일반 객체 반복에 대해 많이 논의하지 않습니다).

여태까지는 그런대로 잘됐다. 사전을 반복하는 것은 그리 어렵지 않습니다. 그렇죠? 문제는 반복 중에 배열/객체가 변경될 수 있다는 것을 깨달을 때 시작됩니다. 여러 가지 방법으로 발생할 수 있습니다.

  • foreach ($arr as &$v) 를 사용하여 참조로 반복하면 $arr 이 참조로 바뀌고 반복 중에 변경할 수 있습니다.
  • PHP 5에서는 값으로 반복하더라도 동일하게 적용되지만 배열은 미리 참조였습니다. $ref =& $arr; foreach ($ref as $v)
  • 객체에는 바이 핸들 전달 의미 체계가 있으며, 이는 대부분의 실용적인 목적에서 참조처럼 동작함을 의미합니다. 따라서 객체는 반복 중에 항상 변경할 수 있습니다.

반복 중에 수정을 허용하는 문제는 현재 있는 요소가 제거되는 경우입니다. 포인터를 사용하여 현재 있는 배열 요소를 추적한다고 가정해 보겠습니다. 이 요소가 이제 해제되면 매달린 포인터가 남게 됩니다(일반적으로 segfault가 발생함).

이 문제를 해결하는 방법에는 여러 가지가 있습니다. PHP 5와 PHP 7은 이와 관련하여 크게 다르며 다음에서 두 동작에 대해 설명하겠습니다. 요약하면 PHP 5의 접근 방식은 다소 멍청하고 모든 종류의 이상한 엣지 케이스 문제로 이어지는 반면, PHP 7의 더 복잡한 접근 방식은 더 예측 가능하고 일관된 동작을 초래한다는 것입니다.

마지막 예비 단계로 PHP는 메모리를 관리하기 위해 참조 카운팅과 쓰기 시 복사를 사용한다는 점에 유의해야 합니다. 이는 값을 "복사"하면 실제로 이전 값을 재사용하고 참조 카운트(refcount)를 증가시킨다는 것을 의미합니다. 일종의 수정을 수행한 후에만 실제 사본("복제"라고 함)이 수행됩니다. 이 주제에 대한 더 광범위한 소개를 보려면 거짓말을 당하고 있습니다를 참조하십시오.

PHP 5

내부 배열 포인터와 HashPointer

PHP 5의 배열에는 수정을 적절히 지원하는 전용 "내부 배열 포인터"(IAP)가 하나 있습니다. 요소가 제거될 때마다 IAP가 이 요소를 가리키는지 여부를 확인합니다. 그렇다면 다음 요소로 대신 진행됩니다.

foreach 는 IAP를 사용하지만 추가 복잡성이 있습니다. IAP는 하나만 있지만 하나의 어레이는 여러 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) { // ... } }

내부 배열 포인터가 하나만 있는 두 개의 동시 루프를 지원하기 위해 foreach 는 다음과 같은 속임수를 수행합니다. 루프 본문이 실행되기 전에 foreach 는 현재 요소에 대한 포인터와 해당 해시를 foreach HashPointer 합니다. 루프 본문이 실행된 후 IAP가 여전히 존재하는 경우 이 요소로 다시 설정됩니다. 그러나 요소가 제거된 경우 현재 IAP가 있는 곳이면 어디든지 사용합니다. 이 구성표는 거의 일종의 작동하지만, 이를 통해 얻을 수 있는 이상한 동작이 많이 있으며 그 중 일부는 아래에서 설명하겠습니다.

어레이 복제

IAP는 배열의 가시적인 기능( current 기능 제품군을 통해 노출됨)입니다. IAP에 대한 변경 사항은 copy-on-write 의미론에서 수정 사항으로 간주되기 때문입니다. foreach 가 반복되는 배열을 복제해야 하는 경우가 많다는 것을 의미합니다. 정확한 조건은 다음과 같습니다.

  1. 배열이 참조가 아닙니다(is_ref=0). 이 참조가 있다면 그것을 전파 해야하는에이 중복되지 않아야하므로, 다음, 변경합니다.
  2. 배열에 refcount>1이 있습니다. refcount 가 1이면 배열이 공유되지 않고 직접 수정할 수 있습니다.

배열이 복제되지 않은 경우(is_ref=0, refcount=1), 해당 refcount 만 증가합니다(*). 또한 foreach by reference를 사용하면 (중복될 가능성이 있는) 배열이 참조로 바뀝니다.

이 코드를 중복이 발생하는 예로 고려하십시오.

 function iterate($arr) { foreach ($arr as $v) {} } $outerArr = [0, 1, 2, 3, 4]; iterate($outerArr);

여기서, $arr IAP가에 변화 방지하기 위해 중복됩니다 $arr 하기 누출 $outerArr . 위의 조건에서 배열은 참조가 아니며(is_ref=0) 두 곳에서 사용됩니다(refcount=2). 이 요구 사항은 불행하고 차선책 구현의 아티팩트입니다(여기서 반복하는 동안 수정에 대한 우려가 없으므로 처음부터 IAP를 사용할 필요가 없습니다).

(*) refcount 증가시키는 것은 무해하게 들리지만 COW(copy-on-write) 의미론을 위반합니다. 즉, refcount=2 배열의 IAP를 수정하려는 반면 COW는 수정이 refcount에서만 수행될 수 있음을 나타냅니다. =1 값. 이 위반은 반복 배열의 IAP 변경을 관찰할 수 있기 때문에 사용자가 볼 수 있는 동작 변경(COW는 일반적으로 투명함)을 초래합니다. 대신, 세 가지 "유효한" 옵션은 a) 항상 복제, b) refcount 증가시키지 않아 루프에서 반복 배열이 임의로 수정되도록 허용하거나 c) IAP를 전혀 사용하지 않는 것입니다( PHP 7 솔루션).

직위 승격 명령

아래 코드 샘플을 제대로 이해하기 위해 알아야 할 마지막 구현 세부 정보가 있습니다. 일부 데이터 구조를 반복하는 "정상적인" 방법은 의사 코드에서 다음과 같이 보일 것입니다.

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

그러나 foreach 는 다소 특별한 눈송이이므로 작업을 약간 다르게 선택합니다.

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

즉, 루프 본문이 실행 되기 전에 배열 포인터가 이미 앞으로 이동합니다. 즉, 루프 본문이 요소 $i 에서 작동하는 동안 IAP는 이미 요소 $i+1 있습니다. 이것이 반복 중 수정을 보여주는 코드 샘플이 항상 현재 요소가 아닌 다음 unset

예: 테스트 케이스

foreach 구현의 특이성에 대한 대부분의 완전한 인상을 제공해야 하며 몇 가지 예에 대해 논의할 수 있습니다.

테스트 케이스의 동작은 이 시점에서 간단하게 설명할 수 있습니다.

  • 테스트 사례 1과 2에서 $array 는 refcount=1로 시작하므로 foreach refcount 만 증가합니다. 루프 본문이 이후에 배열(해당 지점에서 refcount=2를 가짐)을 수정하면 해당 지점에서 복제가 발생합니다. $array 의 수정되지 않은 복사본에 대한 작업을 계속할 것입니다.

  • 테스트 사례 3에서 다시 한 번 배열이 복제되지 않으므로 foreach $array 변수의 IAP를 수정합니다. 반복이 끝나면 IAP는 NULL(반복이 완료 each false 를 반환하여 나타냅니다.

  • 테스트 케이스 4와 5에서 eachreset 은 모두 참조에 의한 기능입니다. $array refcount=2 를 가지므로 복제해야 합니다. 따라서 foreach 는 별도의 배열에서 다시 작동합니다.

예: foreach에서 current

다양한 복제 동작을 보여주는 좋은 방법은 foreach current() 함수의 동작을 관찰하는 것입니다. 다음 예를 고려하십시오.

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

current() 는 배열을 수정하지 않더라도 참조에 의한 함수(실제로는 선호 참조)라는 것을 알아야 합니다. 모두 by-ref인 next 와 같은 다른 모든 기능과 함께 잘 작동하기 위해서는 그래야만 합니다. 참조에 의한 전달은 배열이 분리되어야 함을 의미하므로 $arrayforeach-array 가 달라집니다. 1 대신 2 를 얻는 이유도 위에서 언급했습니다. foreach 는 사용자 코드를 실행한 후에가 아니라 실행 하기 전에 배열 포인터를 전진시킵니다. 따라서 코드가 첫 번째 요소에 있더라도 foreach 이미 두 번째 요소에 대한 포인터를 전진시켰습니다.

이제 약간의 수정을 시도해 보겠습니다.

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

여기에서는 is_ref=1인 경우가 있으므로 배열이 복사되지 않습니다(위와 같이). current() 함수에 전달할 때 배열을 더 이상 복제할 필요가 없습니다. 따라서 current()foreach 는 동일한 배열에서 작동합니다. foreach 가 포인터를 전진시키는 방식으로 인해 여전히 오프바이원(off-by-one) 동작을 볼 수 있습니다.

by-ref 반복을 수행할 때도 동일한 동작을 얻습니다.

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

여기서 중요한 부분은 foreach가 $array 를 is_ref=1로 만들므로 기본적으로 위와 같은 상황이 발생한다는 것입니다.

또 다른 작은 변형, 이번에는 배열을 다른 변수에 할당합니다.

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

$array 의 refcount는 루프가 시작될 때 2이므로 한 번 실제로는 사전에 복제를 수행해야 합니다. 따라서 $array 와 foreach가 사용하는 배열은 처음부터 완전히 분리됩니다. 이것이 루프 이전에 있었던 IAP의 위치를 얻는 이유입니다(이 경우 첫 번째 위치에 있음).

예: 반복 중 수정

반복 중 수정 사항을 설명하려는 것은 모든 foreach 문제가 발생한 곳이므로 이 경우에 대한 몇 가지 예를 고려하는 데 도움이 됩니다.

동일한 배열에 대한 다음 중첩 루프를 고려하십시오(여기서 by-ref 반복은 실제로 동일한지 확인하는 데 사용됨).

 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)

여기서 예상되는 부분은 1 이 제거되었기 때문에 출력에서 (1, 2) 가 누락되었다는 것입니다. 아마도 예상치 못한 것은 외부 루프가 첫 번째 요소 다음에 중지된다는 것입니다. 왜 그런 겁니까?

그 이유는 위에서 설명한 중첩 루프 해킹입니다. 루프 본문이 실행되기 전에 현재 IAP 위치와 해시가 HashPointer 백업됩니다. 루프 본문 이후에 복원되지만 요소가 여전히 존재하는 경우에만, 그렇지 않으면 현재 IAP 위치(무엇이든 상관없음)가 대신 사용됩니다. 위의 예에서 이것이 바로 그 경우입니다. 외부 루프의 현재 요소가 제거되었으므로 내부 루프에서 이미 완료된 것으로 표시된 IAP를 사용합니다!

HashPointer 백업+복원 메커니즘의 또 다른 결과는 reset() foreach 영향을 미치지 않는다는 것입니다. 예를 들어 다음 코드는 reset() 이 전혀 없는 것처럼 실행됩니다.

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

그 이유는 reset() 이 IAP를 일시적으로 수정하는 동안 루프 본문 이후의 현재 foreach 요소로 복원되기 때문입니다. reset() 이 루프에 영향을 미치도록 하려면 백업/복원 메커니즘이 실패하도록 현재 요소를 추가로 제거해야 합니다.

 $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

그러나 이러한 예는 여전히 제정신입니다. HashPointer 복원은 요소에 대한 포인터와 해당 해시를 사용하여 요소가 여전히 존재하는지 여부를 확인한다는 것을 기억하면 진정한 재미가 시작됩니다. 하지만: 해시에는 충돌이 있고 포인터를 재사용할 수 있습니다! 즉, 배열 키를 신중하게 선택하면 foreach 가 제거된 요소가 여전히 존재한다고 믿게 만들어 해당 요소로 바로 이동할 수 있습니다. 예:

 $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

여기서 우리는 일반적으로 이전 규칙에 따라 1, 1, 3, 4 어떻게 되는지 'FYFY' 는 제거된 요소 'EzFY' 와 동일한 해시를 가지며 할당자는 요소를 저장하기 위해 동일한 메모리 위치를 재사용합니다. 따라서 foreach는 새로 삽입된 요소로 직접 점프하여 루프를 단축시킵니다.

루프 동안 반복 엔티티 대체

마지막으로 언급하고 싶은 이상한 경우는 PHP를 사용하여 루프 중에 반복되는 엔터티를 대체할 수 있다는 것입니다. 따라서 한 어레이에서 반복을 시작한 다음 중간에 다른 어레이로 교체할 수 있습니다. 또는 배열에서 반복을 시작한 다음 객체로 교체하십시오.

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

이 경우에서 볼 수 있듯이 PHP는 대체가 발생하면 처음부터 다른 엔터티를 반복하기 시작합니다.

PHP 7

해시 테이블 반복자

아직도 기억하신다면 배열 반복의 주요 문제는 반복 도중에 요소 제거를 처리하는 방법이었습니다. PHP 5는 이러한 목적을 위해 단일 내부 배열 포인터(IAP)를 사용했는데, 이는 하나의 배열 포인터가 여러 동시 foreach 루프 그 위에 reset() 등과의 상호 작용을 지원하기 위해 확장되어야 했기 때문에 다소 차선책이었습니다.

PHP 7은 다른 접근 방식을 사용합니다. 즉, 임의의 양의 외부 안전한 해시 테이블 반복기 생성을 지원합니다. 이러한 반복자는 배열에 등록되어야 하며, 그 시점부터 IAP와 동일한 의미를 갖습니다. 배열 요소가 제거되면 해당 요소를 가리키는 모든 해시 테이블 반복자가 다음 요소로 진행됩니다.

이는 foreach 가 더 이상 IAP 사용하지 않음을 의미합니다. foreach current() 등의 결과에 절대적으로 영향을 미치지 않으며 자체 동작은 reset() 등과 같은 함수의 영향을 받지 않습니다.

어레이 복제

PHP 5와 PHP 7 사이의 또 다른 중요한 변경 사항은 배열 복제와 관련이 있습니다. 이제 IAP가 더 이상 사용되지 않으므로 refcount 배열 반복은 모든 경우에 refcount 증분만 수행합니다(배열을 복제하는 대신). foreach 루프 중에 어레이가 수정되면 그 시점에서 복제가 발생하고(copy-on-write에 따라) foreach 는 이전 어레이에서 계속 작동합니다.

대부분의 경우 이 변경 사항은 투명하며 성능 향상 외에 다른 영향은 없습니다. 그러나 다른 동작이 발생하는 한 가지 경우가 있습니다. 즉, 배열이 사전에 참조였던 경우입니다.

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

이전에는 참조 배열의 값별 반복이 특별한 경우였습니다. 이 경우 중복이 발생하지 않았으므로 반복 중에 배열의 모든 수정 사항이 루프에 반영됩니다. PHP 7에서는 이 특별한 경우가 사라졌습니다. 배열의 값에 의한 반복은 루프 중 수정 사항을 무시하고 항상 원래 요소에서 계속 작동합니다.

물론 이것은 참조에 의한 반복에는 적용되지 않습니다. 참조로 반복하면 모든 수정 사항이 루프에 반영됩니다. 흥미롭게도 일반 객체의 값별 반복에서도 마찬가지입니다.

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

이것은 객체의 핸들에 의한 의미를 반영합니다(즉, 값에 의한 컨텍스트에서도 참조처럼 동작합니다).

테스트 사례부터 시작하여 몇 가지 예를 살펴보겠습니다.

  • 테스트 케이스 1과 2는 동일한 출력을 유지합니다. 값별 배열 반복은 항상 원래 요소에서 계속 작동합니다. (이 경우 refcounting 및 복제 동작도 정확히 동일합니다).

  • 테스트 사례 3 변경: Foreach 는 더 이상 IAP를 사용하지 않으므로 each() 는 루프의 영향을 받지 않습니다. 이전과 이후에 동일한 출력이 나타납니다.

  • 테스트 사례 4와 5는 동일하게 유지됩니다. each()reset() 은 IAP를 변경하기 전에 어레이를 복제하지만 foreach 여전히 원래 어레이를 사용합니다. (배열을 공유하더라도 IAP 변경이 중요하지 않다는 것은 아닙니다.)

두 번째 예제 세트는 다른 reference/refcounting 구성 current() current() 는 루프의 영향을 완전히 받지 않으므로 반환 값은 항상 동일하게 유지되므로 더 이상 의미가 없습니다.

그러나 반복 중에 수정을 고려할 때 몇 가지 흥미로운 변경 사항이 있습니다. 나는 당신이 새로운 행동을 더 건전하게 찾을 수 있기를 바랍니다. 첫 번째 예:

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

보시다시피, 외부 루프는 첫 번째 반복 후에 더 이상 중단되지 않습니다. 그 이유는 이제 두 루프에 완전히 별도의 해시 테이블 반복자가 있고 더 이상 공유 IAP를 통해 두 루프의 교차 오염이 발생하지 않기 때문입니다.

지금 수정된 또 다른 이상한 경우는 동일한 해시를 갖는 요소를 제거하고 추가할 때 얻는 이상한 효과입니다.

 $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

이전에 HashPointer 복원 메커니즘은 제거된 요소와 동일한 것처럼 "보이기" 때문에 새 요소로 바로 이동했습니다(해시와 포인터 충돌로 인해). 더 이상 요소 해시에 의존하지 않으므로 더 이상 문제가 되지 않습니다.


NikiC

예제 3에서는 배열을 수정하지 않습니다. 다른 모든 예에서는 내용이나 내부 배열 포인터를 수정합니다. 이것은 할당 연산자의 의미 때문에 PHP 배열의 경우 중요합니다.

PHP에서 배열에 대한 할당 연산자는 게으른 클론처럼 작동합니다. 배열을 포함하는 다른 변수에 하나의 변수를 할당하면 대부분의 언어와 달리 배열이 복제됩니다. 그러나 필요한 경우가 아니면 실제 복제가 수행되지 않습니다. 즉, 변수 중 하나가 수정된 경우에만 복제가 수행됩니다(기록 중 복사).

다음은 예입니다.

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

foreach 가 배열에 대한 참조를 사용하여 일종의 반복자를 생성한다고 쉽게 상상할 수 있습니다. 이 참조는 내 예의 $b 와 정확히 동일하게 작동합니다. 그러나 참조와 함께 반복자는 루프 동안에만 활성화되고 둘 다 폐기됩니다. 이제 3을 제외한 모든 경우에 루프 중에 배열이 수정되고 이 추가 참조가 살아 있음을 알 수 있습니다. 이것은 클론을 트리거하고 여기에서 무슨 일이 일어나고 있는지 설명합니다!

이 copy-on-write 동작의 또 다른 부작용에 대한 훌륭한 기사가 있습니다. PHP 삼항 연산자: 빠르거나 그렇지 않습니까?


linepogl

foreach() 작업할 때 주의해야 할 몇 가지 사항:

a) foreach 는 원본 어레이 의 예상 복사본 에서 작동합니다. 이는 foreach Notes/User comment 에 prospected copy 이 생성되지 않을 때까지 또는 그렇지 않은 한 foreach() 가 SHARED 데이터 스토리지를 갖게 됨을 의미합니다.

b) 잠재 카피를 유발하는 요인은 무엇입니까? copy-on-write 정책에 따라 생성됩니다. foreach() 전달된 배열이 변경될 때마다 원래 배열의 복제본이 생성됩니다.

c) 원래 배열과 foreach() DISTINCT SENTINEL VARIABLES 를 갖습니다. 즉, 원래 배열과 foreach 대한 것입니다. 아래 테스트 코드를 참조하십시오. SPL , 반복자배열 반복자 .

스택 오버플로 질문 PHP의 'foreach' 루프에서 값이 재설정되었는지 확인하는 방법은 무엇입니까? 귀하의 질문에 대한 사례(3,4,5)를 다룹니다.

다음 예는 each() 및 reset()이 foreach() 반복자의 SENTINEL 변수 (for example, the current index variable)

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

산출:

 each() Original (outside): 0 => 1 foreach: 0 => 1 each() Original(inside): 1 => 2 --------Iteration-------- foreach: 1 => 2 each() Original(inside): 2 => 3 --------Iteration-------- foreach: 2 => 3 each() Original(inside): 3 => 4 --------Iteration-------- foreach: 3 => 4 each() Original(inside): 4 => 5 --------Iteration-------- Resetting original array pointer foreach: 4 => 5 each() Original(inside): 0=>1 --------Iteration-------- each() Original (outside): 1 => 2

sakhunzai

PHP 7에 대한 참고 사항

이 답변이 인기를 얻었기 때문에 이 답변을 업데이트하려면: 이 답변은 PHP 7부터 더 이상 적용되지 않습니다. " 이전 버전과 호환되지 않는 변경 사항 "에서 설명한 대로 PHP 7에서 foreach는 배열 복사본에서 작동하므로 배열 자체의 모든 변경 사항은 foreach 루프에 반영되지 않습니다. 자세한 내용은 링크에서.

설명( php.net 인용):

첫 번째 형식은 array_expression에서 지정한 배열을 반복합니다. 각 반복에서 현재 요소의 값이 $value에 할당되고 내부 배열 포인터가 1만큼 전진합니다(따라서 다음 반복에서 다음 요소를 보게 됩니다).

따라서 첫 번째 예에서는 배열에 하나의 요소만 있고 포인터가 이동될 때 다음 요소가 존재하지 않습니다. 따라서 새 요소를 추가한 후에 foreach는 그것을 마지막 요소로 이미 "결정"했기 때문에 끝납니다.

두 번째 예에서는 두 개의 요소로 시작하고 foreach 루프는 마지막 요소에 없으므로 다음 반복에서 배열을 평가하므로 배열에 새 요소가 있음을 인식합니다.

나는 이것이 모두 문서에 있는 설명의 각 반복 부분의 결과라고 생각합니다 foreach {} 의 코드를 호출하기 전에 모든 논리를 수행한다는 것을 의미합니다.

테스트 케이스

이것을 실행하면:

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

다음과 같은 결과를 얻을 수 있습니다.

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

즉, "제시간에" 수정되었기 때문에 수정을 수락하고 통과했습니다. 하지만 이렇게 하면:

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

당신은 얻을 것이다:

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

즉, 배열이 수정되었지만 foreach 이미 배열의 마지막 요소에 있을 때 수정했기 때문에 더 이상 반복하지 않기로 "결정"하고 새 요소를 추가했지만 "너무 늦게" 추가했습니다. 반복되지 않았습니다.

자세한 설명은 PHP 'foreach'가 실제로 작동하는 방식 에서 읽을 수 있습니다. 이것은 이 행동 뒤에 숨은 내부를 설명합니다.


dkasipovic

PHP 매뉴얼에서 제공하는 문서에 따라.

각 반복에서 현재 요소의 값은 $v에 할당되고 내부
배열 포인터는 1만큼 전진합니다(따라서 다음 반복에서 다음 요소를 보게 됩니다).

따라서 첫 번째 예에 따라:

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

$array 에는 단일 요소만 있으므로 foreach 실행에 따라 1 $v 할당하고 포인터를 이동할 다른 요소가 없습니다.

그러나 두 번째 예에서:

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

$array 에는 두 개의 요소가 있으므로 이제 $array는 0 인덱스를 평가하고 포인터를 1만큼 이동합니다. 루프의 첫 번째 반복을 위해 $array['baz']=3; 참조로 전달합니다.


user3535130

경험 많은 개발자라도 PHP가 foreach 루프에서 배열을 처리하는 방식에 혼란스러워하기 때문에 좋은 질문입니다. 표준 foreach 루프에서 PHP는 루프에서 사용되는 배열의 복사본을 만듭니다. 루프가 완료된 직후 복사본이 삭제됩니다. 이것은 간단한 foreach 루프의 작업에서 투명합니다. 예를 들어:

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

이것은 다음을 출력합니다.

 apple banana coconut

따라서 복사본이 생성되지만 개발자는 루프 내에서 또는 루프가 완료된 후에 원본 배열이 참조되지 않기 때문에 이를 알아차리지 못합니다. 그러나 루프에서 항목을 수정하려고 하면 완료했을 때 수정되지 않은 것을 알 수 있습니다.

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

이것은 다음을 출력합니다.

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

$item에 값을 명확하게 할당했음에도 불구하고 원본의 변경 사항을 알 수 없습니다. 실제로 원본과의 변경 사항은 없습니다. 이것은 작업 중인 $set의 복사본에 나타나는 $item에 대해 작업하고 있기 때문입니다. 다음과 같이 $item을 참조로 가져와 이를 재정의할 수 있습니다.

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

이것은 다음을 출력합니다.

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

따라서 $item이 참조에 의해 작동될 때 $item에 대한 변경 사항은 원래 $set의 멤버에 적용된다는 것이 명백하고 관찰 가능합니다. 참조로 $item을 사용하면 PHP가 배열 복사본을 생성하는 것도 방지할 수 있습니다. 이를 테스트하기 위해 먼저 사본을 보여주는 빠른 스크립트를 보여줍니다.

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

이것은 다음을 출력합니다.

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

예제와 같이 PHP는 $set을 복사하여 반복문에 사용했지만, 루프 내부에서 $set을 사용했을 때 PHP는 복사된 배열이 아닌 원래 배열에 변수를 추가했습니다. 기본적으로 PHP는 루프 실행과 $item 할당을 위해 복사된 배열만 사용합니다. 이 때문에 위의 루프는 3번만 실행되며 원래 $set의 끝에 다른 값을 추가할 때마다 원래 $set에 6개의 요소가 남아 있지만 무한 루프에 들어가지는 않습니다.

그러나 앞에서 언급한 것처럼 $item을 참조로 사용했다면 어떻게 될까요? 위의 테스트에 추가된 단일 문자:

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

무한 루프가 발생합니다. 이것은 실제로 무한 루프입니다. 스크립트를 직접 종료하거나 OS의 메모리가 부족해질 때까지 기다려야 합니다. PHP에서 메모리가 매우 빨리 부족하도록 스크립트에 다음 줄을 추가했습니다. 이러한 무한 루프 테스트를 실행하려는 경우에도 동일한 작업을 수행하는 것이 좋습니다.

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

따라서 무한 루프가 있는 이 이전 예제에서 PHP가 반복할 배열의 복사본을 생성하도록 작성된 이유를 알 수 있습니다. 사본이 생성되고 루프 구조 자체에서만 사용되는 경우 배열은 루프가 실행되는 동안 정적 상태를 유지하므로 문제가 발생하지 않습니다.


Hrvoje Antunović

PHP foreach 루프는 Indexed arrays , Associative arraysObject public variables 와 함께 사용할 수 있습니다.

foreach 루프에서 php가 하는 첫 번째 일은 반복될 배열의 복사본을 생성하는 것입니다. 그런 다음 PHP는 원래 배열이 아닌 copy 이는 아래 예에서 보여줍니다.

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

이 외에도 php는 iterated values as a reference to the original array value 사용할 수 있습니다. 이것은 아래에 설명되어 있습니다.

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

참고: original array indexes references 로 사용할 수 없습니다.

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


Pranav Rana

출처 : http:www.stackoverflow.com/questions/10057671/how-does-php-foreach-actually-work

반응형