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

여기 앞에 제가 뭘 알고 있는지 말씀드리겠습니다.foreach사용법입니다.이 질문은 보닛 아래에서 어떻게 동작하는지에 관한 것입니다.또, 「이것이 어레이를 루프 하는 방법입니다.」라고 하는 식으로 대답하고 싶지 않습니다.foreach“.


오랫동안 나는 그렇게 생각했다.foreach어레이 자체를 조작했습니다.그 후, 어레이의 카피와 함께 동작하는 것에 대한 많은 참조를 발견해, 이것으로 이야기가 끝난다고 생각하고 있습니다.하지만 최근에 그 문제에 대해 논의를 시작했고, 약간의 실험 결과 이것이 사실 100% 사실이 아니라는 것을 알게 되었다.

제 말뜻을 보여드릴게요.다음의 테스트 케이스에서는, 다음의 어레이로 작업을 실시합니다.

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

테스트 케이스 1:

foreach ($array as $item) {   echo "$itemn";   $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 "$itemn"; }  print_r($array);  /* Output in loop:    1 2 3 4 5    $array after loop: 1 3 4 5 6 7 */ 

이것은 초기 결론을 뒷받침합니다.루프 중에 소스 어레이의 복사본으로 작업하고 있습니다.그렇지 않으면 루프 중에 변경된 값이 표시됩니다.근데…

매뉴얼을 보면 다음과 같은 문구가 있습니다.

Forech가 처음 실행을 시작하면 내부 어레이 포인터가 어레이의 첫 번째 요소로 자동으로 리셋됩니다.

맞아…이것은 을 시사하는 것 같다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 "$itemn"; }  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 "$itemn";   each($array); }  /* Output: 1 2 3 4 5 */ 

테스트 케이스 5:

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

…거기서는 전혀 예상하지 못했던 것이 아닙니다.사실 그것은 “출처 복사” 이론을 뒷받침하는 것으로 보입니다.


질문

이게 무슨 일이야?제 C-fu는 PHP 소스 코드만으로 올바른 결론을 도출할 수 없기 때문에 누군가 영어로 번역해 주시면 감사하겠습니다.

내가 보기엔 인 것 같다foreach는 배열 복사본과 함께 작동하지만 소스 배열의 배열 포인터를 루프 후 배열 끝으로 설정합니다.

  • 이게 맞습니까? 전체 내용인가요?
  • 그렇지 않다면, 실제로 무엇을 하고 있을까요?
  • 어레이 포인터를 조정하는 기능을 사용하는 경우가 있습니까?each(),reset()(등) 중foreach루프의 결과에 영향을 줄 수 있을까요?


질문에 대한 답변



foreach는 다음 3종류의 값에 대한 반복을 지원합니다.

이하에서는, 다른 케이스에서 반복이 어떻게 기능하는지를 정확하게 설명하겠습니다.단연코 가장 간단한 경우는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();     /* ... */ } 

내부 클래스의 경우, 실제 메서드 호출은 기본적으로는 내부 API를 사용하여 회피됩니다.IteratorC레벨의 인터페이스입니다.

어레이와 플레인 오브젝트의 반복은 훨씬 복잡합니다.우선, PHP에서 “array”는 정말로 순서가 매겨진 사전이고 그것들은 이 순서에 따라 통과됩니다(삽입 순서와 일치합니다).sort이는 키의 자연스러운 순서(다른 언어의 리스트가 동작하는 경우가 많다) 또는 정의된 순서가 전혀 없는 것(다른 언어의 딕셔너리가 동작하는 경우가 많다)과는 반대됩니다.

오브젝트 속성은 값에 대한 다른 (순서 있는) 사전 속성 이름과 일부 가시성 처리로 표시될 수 있으므로 오브젝트에도 동일하게 적용됩니다.대부분의 경우 객체 속성은 실제로 이렇게 다소 비효율적인 방식으로 저장되지 않습니다.그러나 개체를 통해 반복을 시작하면 일반적으로 사용되는 팩된 표현이 실제 사전으로 변환됩니다.이 시점에서 플레인 객체의 반복은 어레이의 반복과 매우 비슷해집니다(이 때문에 플레인 객체의 반복에 대해서는 여기서 설명하지 않습니다).

지금까지는 좋아.사전을 보면서 반복하는 건 어렵지 않죠?이 문제는 반복 중에 어레이/개체가 변경될 수 있다는 것을 깨달았을 때 시작됩니다.여기에는 여러 가지 방법이 있습니다.

  • 참조로 반복하는 경우foreach ($arr as &$v)그리고나서$arr참조로 변환되어 반복 중에 변경할 수 있습니다.
  • PHP 5에서는 값을 기준으로 반복해도 동일하게 적용되지만 어레이는 미리 참조되었습니다.$ref =& $arr; foreach ($ref as $v)
  • 오브젝트에는 by-handle passing semantics가 있습니다.이것은, 대부분의 실용적인 목적을 위해서, 오브젝트가 참조와 같이 동작하는 것을 의미합니다.따라서 개체는 반복 중에 언제든지 변경할 수 있습니다.

반복 중에 변경을 허용하는 문제는 현재 사용 중인 요소가 제거되는 경우입니다.포인터를 사용하여 현재 배치되어 있는 어레이 요소를 추적한다고 가정합니다.이 요소가 해방되면 (일반적으로 seg fault를 발생시키는) 점멸 포인터가 남습니다.

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

마지막으로 PHP는 참조 카운트와 Copy-on-Write를 사용하여 메모리를 관리합니다.즉, 값을 “복사”할 경우 실제로 이전 값을 재사용하고 참조 개수(refcount)를 늘리기만 하면 됩니다.어떠한 종류의 수정을 실시한 후에만, 실제의 카피( 「복제」라고 불립니다)가 행해집니다.이 항목에 대한 자세한 내용은 속임을 참조하십시오.

PHP 5

내부 어레이 포인터 및 Hash Pointer

PHP 5의 어레이에는 1개의 전용 IAP(Internal Array Pointer)가 있어 적절한 수정을 지원합니다.요소가 삭제될 때마다 IAP가 이 요소를 가리키는지 여부가 체크됩니다.이 경우 대신 다음 요소로 넘어갑니다.

하는 동안에foreach는 IAP를 사용하고 있습니다.또, 다음의 복잡성이 있습니다.IAP는 1개뿐이지만 1개의 어레이를 여러 개의 어레이에 포함할 수 있습니다.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) {         // ...     } } 

내부 어레이 포인터를 1개만 사용하여 2개의 루프를 동시에 지원하려면foreach는 다음 셰니건을 수행합니다.루프 본체가 실행되기 전에foreach현재 요소에 대한 포인터와 해당 해시를 포치별로 백업합니다.HashPointer루프 본체가 실행되면 IAP가 아직 존재하는 경우 이 요소로 돌아갑니다.요소가 제거된 경우 IAP가 현재 있는 모든 위치를 사용합니다.이 계획은 거의 효과가 있지만, 많은 이상한 행동들이 있습니다. 그 중 몇 가지 이상한 행동을 통해 얻을 수 있습니다.

어레이 복제

IAP는 어레이의 가시적인 기능(를 통해 표시됨)입니다.currentCopy-on-Write 시멘틱스에서는 IAP 카운트가 변경되어 있습니다.이것은, 유감스럽게도, 라는 것을 의미합니다.foreach많은 경우 반복하고 있는 어레이를 복제해야 합니다.정확한 조건은 다음과 같습니다.

  1. 배열이 참조가 아닙니다(is_ref=0).참조일 경우 변경 내용이 전파되기 때문에 중복되어서는 안 됩니다.
  2. 어레이의 refcount >1 입니다.한다면refcount어레이는 공유되지 않으며 직접 변경할 수 있습니다.

어레이가 복제되지 않은 경우(is_ref=0, refcount=1) 어레이만 복제됩니다.refcount(*)가 증가합니다.게다가 만약foreach참조를 사용하면 (복제된) 배열이 참조로 변환됩니다.

이 코드는 중복이 발생하는 예로서 간주합니다.

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여기서는 무해하게 들리지만 Copy-on-Write(COW; 쓰기 시 복사) 시멘틱스에 위반됩니다.즉, refcount=2 어레이의 IAP를 수정하는 반면, COW는 refcount=1 값에 대해서만 수정을 수행할 수 있다고 지시합니다.이 위반으로 인해 COW는 통상 투과적인 동작의 변경을 볼 수 있게 됩니다.이는 반복된 어레이의 IAP 변경이 관찰 가능하기 때문입니다.단, 어레이의 첫 번째 비 IAP 변경이 이루어질 때까지입니다.대신에, 3개의 「유효한」옵션은 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(); } 

즉, 루프 본체가 실행되기 전에 어레이 포인터가 이미 으로 이동되어 있습니다.이것은 루프 본체가 요소에서 작동하는 동안$iIAP는 이미 엘리먼트에 있습니다.$i+1이것이 반복 중에 수정되는 코드 샘플이 항상 표시되는 이유입니다.unset현재 요소가 아닌 다음 요소입니다.

예:테스트 케이스

위에서 설명한 세 가지 측면은 다음과 같은 특징에 대해 대부분 완전한 인상을 줄 것입니다.foreach몇 가지 예를 들어 설명하겠습니다.

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

  • 테스트 케이스 1 및 2의 경우$arrayrefcount=1로 시작하므로 다음 항목에 의해 중복되지 않습니다.foreach:만refcount증가합니다.이후 루프 본체가 어레이를 수정하면(그 시점에서 refcount=2가 됩니다), 해당 지점에서 중복이 발생합니다.Forech는 수정되지 않은 복사 작업을 계속합니다.$array.

  • 테스트 케이스 3에서도 어레이는 복제되지 않습니다.foreach의 IAP 를 변경합니다.$array변수.반복 종료 시 IAP는 NULL(반복 완료)로 되어 있습니다.each반환함으로써 나타내다false.

  • 테스트 케이스 4 및 5의 경우 둘 다each그리고.reset참조 함수입니다.$array가 있다refcount=2복제해야 합니다.그렇게 해서foreach다시 다른 어레이로 작업하게 됩니다.

예: 효과current앞바다에

다양한 복제 동작을 보여주는 좋은 방법은 이 동작의 동작을 관찰하는 것입니다.current()안에서 기능하다foreach루프. 다음 예시를 고려해 주세요.

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

여기서 알아두셔야 합니다current()는 by-ref 함수(실제로는 prefer-ref)이지만 배열을 변경하지 않습니다.다른 모든 기능들과 잘 어울리려면 다음과 같이 해야 합니다.next모두 참고인이야By-reference passing은 어레이를 분리해야 함을 의미하며, 따라서$array및 그foreach-array다를 것이다.얻는 이유2대신1에 대해서도 상술하고 있습니다.foreach는 사용자 코드를 실행하기 전에 어레이 포인터를 전진시킵니다.이후가 아닙니다.그래서 코드가 첫 번째 요소에 있더라도foreach이미 포인터를 두 번째 포인트로 진행했습니다.

이제 작은 수정을 시도하겠습니다.

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

여기 is_ref=1 대소문자가 있으므로 위와 같이 배열은 복사되지 않습니다.그러나 이제 참조가 되었으므로 by-ref로 전달될 때 어레이를 복제할 필요가 없습니다.current()기능.따라서current()그리고.foreach같은 어레이로 작업합니다.하지만 여전히 하나씩의 행동을 볼 수 있습니다.foreach포인터를 전진시킵니다.

by-ref 반복 실행 시 다음과 같은 동작이 발생합니다.

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

여기서 중요한 부분은 포어치가 만들어 줄 것이다.$arrayis_ref=1은 참조에 의해 반복되므로 기본적으로 위와 같은 상황이 됩니다.

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

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

여기서의 리카운트$array루프가 시작되면 2가 되므로 이번에는 실제로 복제를 먼저 해야 합니다.따라서$arrayForeach가 사용하는 어레이는 처음부터 완전히 분리됩니다.따라서 루프 전에 어디에 있든 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, 2)요소 때문에 출력에서 누락되었습니다.1삭제되었습니다.아마도 예상치 못한 것은 첫 번째 요소 이후에 외부 루프가 멈춘다는 것입니다.왜 그런 것일까요?

그 배경에는 위에서 설명한 네스트 루프 해킹이 있습니다.루프 본체가 실행되기 전에 현재 IAP 위치와 해시는HashPointer루프 본체 후에 복원됩니다.단, 엘리먼트가 아직 존재하는 경우에만 현재 IAP 위치(무엇이든)가 대신 사용됩니다.위의 예에서는 다음과 같습니다.외부 루프의 현재 요소가 제거되었으므로 내부 루프가 이미 종료된 것으로 표시된 IAP가 사용됩니다.

의 다른 결과HashPointerbackup+restore 메커니즘은 IAP에 변경을 가하는 것입니다.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 

하지만, 그 예들은 여전히 제정신이다.진짜 재미는 기억하실 때 시작돼요HashPointerrestore는 요소 및 해당 해시에 대한 포인터를 사용하여 요소가 아직 존재하는지 여부를 확인합니다.단, 해시에는 충돌이 있어 포인터를 재사용할 수 있습니다!즉, 어레이 키를 신중하게 선택함으로써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'allocator는 같은 메모리 위치를 재사용하여 요소를 저장합니다.따라서 포어치는 새로 삽입된 요소로 직접 점프하여 루프를 단축합니다.

루프 중에 반복된 엔티티 대체

마지막으로 언급하고 싶은 이상한 경우는 루프 중에 반복된 엔티티를 PHP로 대체할 수 있다는 것입니다.따라서 한 어레이에서 반복 작업을 시작한 후 중간에 다른 어레이로 교체할 수 있습니다.또는 어레이에서 반복을 시작한 다음 개체로 대체합니다.

$arr = [1, 2, 3, 4, 5]; $obj = (object) [6, 7, 8, 9, 10];  $ref =& $arr; foreach ($ref as $val) {     echo "$valn";     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()etc 및 그 자체의 동작은 결코 다음과 같은 기능에 의해 영향을 받지 않습니다.reset()기타.

어레이 복제

PHP 5와 PHP 7의 또 다른 중요한 변경 사항은 어레이 복제와 관련이 있습니다.IAP가 사용되지 않게 되었기 때문에 값별 배열 반복에서는refcount(어레이의 중복이 아닌) 모든 경우에 increment.어레이가 변경되었을 경우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 */ 

이는 객체의 by-handle 의미론을 반영한다(즉, by-value 컨텍스트에서도 참조와 같이 동작한다).

테스트 사례에서 몇 가지 예를 살펴보겠습니다.

  • 테스트 케이스1과 2는 동일한 출력을 유지합니다.값별 배열 반복은 항상 원래 요소에서 계속 작동합니다(이 경우 짝수).refcounting복제 동작은 PHP 5와 PHP 7에서 완전히 동일합니다).

  • 테스트 케이스 3의 변경:ForeachIAP를 사용하지 않게 되었기 때문에each()는 루프의 영향을 받지 않습니다.전후로 같은 출력이 됩니다.

  • 테스트 케이스 4와 5는 동일하게 유지됩니다.each()그리고.reset()는 IAP를 변경하기 전에 어레이를 복제합니다.foreach는 여전히 원래 어레이를 사용합니다.(어레이가 공유된 경우에도 IAP 변경은 문제가 되지 않습니다).

두 번째 예시는 다음과 같은 동작과 관련되어 있습니다.current()다른 아래에서reference/refcounting설정을 지정합니다.이건 더 이상 말이 안 돼요.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 복원 메커니즘이 새 요소로 바로 이동했는데, 이는 HashPointer가 제거된 요소와 동일한 것처럼 보이기 때문입니다(해시와 포인터의 충돌로 인해).요소 해시에 의존하지 않게 되었기 때문에, 이것은 문제가 되지 않게 되었습니다.




예 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 Ternary 연산자: 빨라요, 안 빨라요?




작업 시 주의사항foreach():

a)foreach는, 원래의 어레이의 예상 카피로 동작합니다.즉,foreach()공유 데이터 스토리지를 보유하게 됩니다.prospected copyNotes/사용자 코멘트 미리 작성되지 않습니다.

b) 예상 카피의 원인은 무엇입니까?예상 복사본은 다음 정책에 따라 생성됩니다.copy-on-write즉, 어레이가 에 전달될 때마다foreach()변경되면 원래 어레이의 클론이 생성됩니다.

c) 원래 어레이와foreach()리터레이터는DISTINCT SENTINEL VARIABLES즉, 1개는 원래 어레이용으로, 다른 1개는 기존 어레이용으로, 다른 1개는foreach; 아래 테스트 코드를 참조하십시오.SPL, 반복기어레이 반복기.

스택 오버플로 질문 PHP의 ‘foreach’ 루프에서 값이 리셋되도록 하는 방법은 질문의 케이스(3,4,5)에 대응합니다.

다음으로 각()과 리셋()이 영향을 미치지 않는 예를 나타냅니다.SENTINEL변수(for example, the current index variable)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/>"; 

출력:

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 



PHP 7에 관한 주의사항

이 답변이 인기를 얻었을 때 업데이트하려면:이 답변은 PHP 7부터는 적용되지 않습니다. “Backward incompatible changes”에서 설명한 바와 같이 PHP 7 foreach는 어레이의 복사본에서 작동하므로 어레이 자체의 변경은 foreach 루프에 반영되지 않습니다.자세한 내용은 링크를 참조하십시오.

설명(php.net에서 참조):

첫 번째 폼은 array_expression에 의해 지정된 어레이를 루프합니다.각 반복에서 현재 요소의 값은 $value에 할당되고 내부 배열 포인터는 1씩 증가합니다(따라서 다음 반복에서는 다음 요소를 참조합니다).

따라서 첫 번째 예에서는 배열에 요소가1개밖에 없고 포인터를 이동하면 다음 요소는 존재하지 않습니다.따라서 새로운 요소를 추가한 후 마지막 요소로 이미 “결정”되어 있기 때문입니다.

두 번째 예에서는 두 개의 요소로 시작하지만 마지막 요소에는 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’가 실제로 작동하는 방법”을 참조하십시오.그래서 이런 행동의 배후에 있는 내적인 이유가 설명되죠




PHP 매뉴얼에서 제공하는 설명서에 따라 주십시오.

반복할 때마다 현재 요소의 값은 $v에 할당되며 내부 값은
어레이 포인터는 1개씩 전진합니다(따라서 다음 반복에서는 다음 요소를 볼 수 있습니다).

첫 번째 예시와 같이:

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

$array1개의 요소만을 가지고 있기 때문에 foreach 실행에 따라 1은 에 할당됩니다.$v포인터를 이동할 수 있는 다른 요소가 없습니다.

그러나 두 번째 예에서는 다음과 같습니다.

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

$array2개의 요소가 있으므로 $array는 제로 인덱스를 평가하고 포인터를 1개씩 이동합니다.루프의 첫 번째 반복을 위해 추가됨$array['baz']=3;참고인으로서