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


2018

나는 그것이 무엇인지 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사본 으로 작동 하지만 루프 후 배열의 끝으로 소스 배열의 배열 포인터를 설정하는 것 같습니다 .

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

5
@DaveRandom PHP 내부 태그가 있습니다.하지만 다른 5 개의 태그 중 어느 것을 대체할지 결정하기 위해 남겨 두겠습니다.
Michael Berkowski

5
삭제 핸들이없는 COW처럼 보입니다
zb '

149
처음에는 또 다른 초보자 질문 인»gosh를 생각했습니다. 문서를 읽어보십시오… 흠, 명확하게 정의되지 않은 행동«. 그런 다음 나는 완전한 질문을 읽었으며, 나는 그것을 좋아한다. 당신은 그것에 약간의 노력을 기울이고 모든 테스트 케이스를 작성했습니다. 추신. 테스트 케이스 4와 5가 동일합니까?
knittl

21
배열 포인터가 닿는 이유에 대한 생각 : PHP는 원래 배열의 내부 배열 포인터를 복사본과 함께 재설정하고 이동해야합니다. 사용자가 현재 값 ( foreach ($array as &$value)) 에 대한 참조를 요청할 수 있기 때문입니다. PHP는 실제로 사본을 반복하지만 원래 배열의 현재 위치를 알아야합니다.
Niko

4
@Sean : IMHO, PHP 문서는 핵심 언어 기능의 뉘앙스를 설명하는 데 실제로 나쁩니다. 많은 임시 특별한 경우가 언어로 구운 때문에하지만 그 ..., 아마도,이다
올리버 찰스 워드

답변:


1660

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

내부 클래스의 경우 본질적으로 IteratorC 레벨 에서 인터페이스를 미러링하는 내부 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는 레퍼런스 카운팅과 COW (Copy-On-Write)를 사용하여 메모리를 관리합니다. 즉, 값을 "복사"하는 경우 실제로는 이전 값을 재사용하고 참조 횟수 (refcount)를 늘리기 만하면됩니다. 일종의 수정을 수행 한 후에 만 ​​실제 사본 ( "중복"이라고 함)이 수행됩니다. 이 주제에 대한보다 광범위한 소개 위해 거짓말을하고 있음을 참조하십시오 .

PHP 5

내부 배열 포인터 및 HashPointer

PHP 5의 배열에는 하나의 전용 "IAP (Internal Array Pointer)"가있어 수정을 올바르게 지원합니다. 요소를 제거 할 때마다 IAP가이 요소를 가리키는 지 확인합니다. 그렇다면 다음 요소로 넘어갑니다.

foreachIAP를 사용 하는 동안 추가 복잡성이 있습니다. 하나의 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다음과 같은 shenanigan을 수행하십시오. 루프 본문이 실행되기 전에 foreach현재 요소에 대한 포인터와 해당 해시를 per-foreach로 백업합니다 HashPointer. 루프 본문이 실행 된 후에도 IAP는 여전히 존재하는 경우이 요소로 다시 설정됩니다. 그러나 요소가 제거 된 경우 IAP가 현재 어디에 있든지 사용합니다. 이 계획은 주로 대부분의 작품이지만, 그로부터 얻을 수있는 이상한 행동이 많이 있으며, 그중 일부는 아래에서 설명합니다.

배열 복제

IAP는 current쓰기 중 복사 시맨틱에서 수정 된 것으로 IAP의 변경 사항이 카운트되는 것처럼 배열의 가시적 인 기능 ( 기능 군을 통해 노출됨 )입니다. 불행히도 이것은 foreach많은 경우 반복되는 배열을 복제해야 한다는 것을 의미합니다 . 정확한 조건은 다음과 같습니다.

  1. 배열이 참조가 아닙니다 (is_ref = 0). 참조 인 경우 변경 사항 이 전파 되는 것으로 간주 되므로 복제해서는 안됩니다.
  2. 배열의 참조 횟수는 1입니다. 경우 refcount1, 다음 배열은 공유되지 않고 우리가 직접 자유롭게 변경할 수있어.

배열이 중복되지 않으면 (is_ref = 0, refcount = 1), refcount증가 만 됩니다 (*). 또한 foreach참조로 사용하는 경우 (잠재적으로 복제 된) 배열이 참조로 바뀝니다.

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

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

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

여기에서 $arrIAP 변경 사항이 $arr로 유출 되지 않도록 복제 됩니다 $outerArr. 위의 조건에서 배열은 참조가 아니며 (is_ref = 0) 두 곳에서 사용됩니다 (refcount = 2). 이 요구 사항은 불행하고 차선책의 구현으로 인해 발생합니다 (여기서 반복하는 동안 수정에 대한 우려가 없으므로 먼저 IAP를 사용할 필요는 없습니다).

(*) refcount여기에서 증가하는 것은 무해하게 들리지만 COW (Copy-On-Write) 시맨틱을 위반합니다. 즉, refcount = 2 배열의 IAP를 수정하려고하지만 COW는 수정이 refcount =에서만 수행 될 수 있음을 나타냅니다. 1 개의 값. 이 위반은 반복 배열의 IAP 변경이 관찰 가능하지만 배열의 첫 번째 비 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가 있음) 해당 시점에서 복제가 발생합니다. Foreach는의 수정되지 않은 사본을 계속 작업 할 것입니다 $array.

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

  • 시험 예 4, 5 모두 eachreset참조로 함수이다. 는 $arrayA가 들어 refcount=2가 중복 될 수있다, 그래서 그것이 그들에게 전달 될 때. 따라서 foreach별도의 배열에서 다시 작동합니다.

예 : currentforeach의 효과

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

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

여기서는 current()배열을 수정하지 않더라도 by-ref 함수 (실제로 prefer-ref) 임을 알아야합니다 . 그것은 next모든 by-ref 와 같은 다른 모든 기능 으로 훌륭하게 플레이하기 위해 있어야합니다 . 참조에 의한 전달은 어레이가 분리되어야하므로 것을 의미 $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 */

여기서 중요한 부분은 foreach가 $array참조로 반복 될 때 is_ref = 1이되므로 기본적으로 위와 동일한 상황이됩니다.

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

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

$array루프가 시작될 때 여기에 대한 참조 횟수는 2이므로, 실제로 중복을 미리해야합니다. 따라서 $arrayforeach에서 사용하는 배열은 처음부터 완전히 분리됩니다. 그렇기 때문에 루프 이전에 IAP 위치를 얻을 수 있습니다 (이 경우 첫 번째 위치에 있음).

예 : 반복 중 수정

반복하는 동안 수정을 고려하는 것은 모든 foreach 문제가 발생한 부분이므로이 경우에 대한 몇 가지 예를 고려하는 데 도움이됩니다.

동일한 배열에 대해 다음과 같은 중첩 루프를 고려하십시오 (참조-반복 반복이 실제로 동일한 지 확인하는 데 사용됨).

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를 사용합니다!

HashPointer백업 + 복원 메커니즘 의 또 다른 결과는 reset()일반적으로 등을 통한 IAP 변경이 영향을 미치지 않습니다 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'제거 된 element와 동일한 해시 를 가지며 '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복제는 배열을 복제하는 대신 증분 만 수행합니다 . 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및 복제 동작은 PHP 5와 PHP 7간에 동일합니다.)

  • 테스트 사례 3 변경 사항 : Foreach더 이상 IAP를 사용하지 않으므로 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 복원 메커니즘이 해시와 포인터 충돌로 인해 제거 된 요소와 동일한 것처럼 "보여"새 요소로 바로 이동했습니다. 더 이상 요소 해시에 의존하지 않기 때문에 더 이상 문제가되지 않습니다.


4
@ 바바 않습니다. 함수에 전달하는 $foo = $array것은 루프 이전 과 동일 합니다.;)
NikiC

32
zval이 무엇인지 모르는 분은 Sara Goleman의 blog.golemon.com/2007/01/youre-being-lied-to.html을
shu zOMG chen

1
사소한 수정 : 버킷이라고 부르는 것이 일반적으로 해시 테이블에서 버킷이라고하는 것이 아닙니다. 일반적으로 Bucket은 해시 크기가 동일한 항목 집합입니다. 일반적으로 항목이라고하는 것에 사용하는 것 같습니다. 연결된 목록은 버킷이 아니라 항목에 있습니다.
unbeli

12
@unbeli PHP에서 내부적으로 사용하는 용어를 사용하고 있습니다. Bucket들 이중 링크 해시 충돌에 대한 목록과 순서를위한 이중 연결리스트의도 부분의 일부)
NikiC

4
위대한 답변. 나는 당신이 어딘가가 iterate($outerArr);아니라고 생각합니다 iterate($arr);.
niahoo

116

예제 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을 제외한 모든 경우에 루프 동안 배열이 수정되는 반면이 추가 참조는 살아 있음을 알 수 있습니다. 그러면 복제가 시작되고 여기에서 무슨 일이 일어나고 있는지 설명합니다!

다음은이 기록 중 복사 동작의 또 다른 부작용에 대한 훌륭한 기사입니다 . PHP Ternary Operator : Fast or not?


codepad.org/OCjtvu8r 예제와 하나의 차이점-값을 변경하면 복사하지 않고 키를 변경하는 경우에만 복사하지 않습니다.
zb '

이것은 참으로 위의 모든 행동 쇼를 설명 않으며, 멋지게 호출에 의해 설명 될 수있는 each()첫 번째 테스트 케이스의 끝에서 우리가 볼 두 번째 요소에 원래 배열 포인트의 배열 포인터, 배열이 중에 수정 된 이후 있음 첫 번째 반복. 이것은 또한 foreach루프의 코드 블록을 실행하기 전에 배열 포인터 를 이동시키는 것을 보여줍니다 . 많은 감사, 이것은 나를 위해 잘 정리합니다.
DaveRandom

49

작업 할 때 참고할 사항 foreach():

a) 원본 어레이 foreach예상 사본 에서 작동합니다 . 이는 각 Notes / User 의견에 대해를 만들지 않을 foreach()때까지 또는 공유되지 않은 공유 데이터 저장소를 갖게 됨을 의미 합니다.prospected copy

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

c) 원래 배열과 foreach()이터레이터는 DISTINCT SENTINEL VARIABLES원래 배열과 다른 배열을 갖 습니다 foreach. 아래 테스트 코드를 참조하십시오. SPL , 반복자배열 반복자 .

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

다음 예제는 each () 및 reset () 이 반복자의 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

2
당신의 대답은 정확하지 않습니다. foreach배열의 잠재적 사본에서 작동하지만 필요한 경우가 아니면 실제 사본을 만들지 않습니다.
linepogl

코드를 통해 잠재적 인 사본을 작성하는 방법과시기를 설명 하시겠습니까? 내 코드 foreach는 배열을 100 % 복사하는 것을 보여줍니다 . 알고 싶어요. 댓글 주셔서 감사합니다
sakhunzai

어레이를 복사하는 데 많은 비용이 듭니다. 사용 중 100,000 요소가 배열 반복 소요 시간을 계산하는 시도 for하거나 foreach. 실제 사본이 발생하지 않기 때문에 둘 사이에는 큰 차이가 없습니다.
linepogl

그런 다음 SHARED data storage까지 또는 예약되지 않았을 때까지 예약되어 있다고 가정 copy-on-write하지만 (내 코드 스 니펫에서) 항상 두 SENTINEL variables개 중 하나에 대한 세트가있을 것 original array입니다.foreach . 감사합니다
sakhunzai

1
예, "예상"사본, 즉 "잠재적"사본입니다. 제안한대로 보호되지 않습니다
sakhunzai

33

PHP 7 참고 사항

이 답변은 어느 정도 인기를 얻음에 따라 업데이트하려면 :이 답변은 더 이상 PHP 7부터 적용되지 않습니다. " 이전 버전과 호환되지 않는 변경 사항 "에 설명 된대로 , PHP 7의 foreach는 어레이의 사본에 대해 작동하므로 어레이 자체의 모든 변경 사항 foreach 루프에는 반영되지 않습니다. 링크에서 자세한 내용을 확인하십시오.

설명 ( php.net 에서 인용 ) :

첫 번째 형식은 array_expression이 제공 한 배열을 반복합니다. 각 반복에서 현재 요소의 값이 $ value에 할당되고 내부 배열 포인터가 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'는 실제로 어떻게 작동합니까? 에서 자세한 설명을 읽을 수 있습니다 . 이 행동의 내부를 설명합니다.


7
나머지 답변을 읽었습니까? foreach는 코드를 실행 하기 전에 다른 시간이 반복되는지 여부를 결정하는 것이 완벽 합니다.
dkasipovic

2
아니요, 배열은 수정되었지만 foreach는 이미 마지막 요소 (반복 시작 시점에 있음)에 있고 "더 이상 반복되지"않기 때문에 "너무 늦었습니다". 두 번째 예에서 반복 시작시 마지막 요소가 아니며 다음 반복 시작시 다시 평가됩니다. 테스트 케이스를 준비하려고합니다.
dkasipovic

1
에서 @AlmaDo 봐 lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 이 반복 할 때 항상 다음 포인터로 설정합니다. 따라서 마지막 반복에 도달하면 NULL 포인터를 통해 완료로 표시됩니다. 그런 다음 마지막 반복에서 키를 추가하면 foreach는이를 인식하지 못합니다.
bwoebi

1
@DKasipovic no. 거기에 완전하고 명확한 설명이 없습니다 (적어도 지금은 내가 틀렸을 수도 있습니다)
Alma Do

4
실제로 @AlmaDo는 자신의 논리를 이해하는 데 결함이있는 것 같습니다 ... 대답은 괜찮습니다.
bwoebi

15

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

각 반복에서 현재 요소의 값이 $ v에 할당되고 내부
배열 포인터가 1 씩 앞당겨집니다 (따라서 다음 반복에서는 다음 요소를 보게됩니다).

따라서 첫 번째 예에 따라 :

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

$array foreach 실행에 따라 하나의 요소 만 갖습니다. $v 포인터를 이동할 다른 요소는 없습니다

그러나 두 번째 예에서 :

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

$array두 개의 요소를 가지므로 이제 $ array는 0 인덱스를 평가하고 포인터를 1 씩 움직입니다. 루프의 첫 번째 반복의 경우 $array['baz']=3;참조로 전달로 추가 됩니다.


13

많은 개발자들, 심지어 경험 많은 개발자들도 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가 작성된 이유를 알 수 있습니다. 루프 구조 자체의 구조에 의해서만 복사본이 만들어지고 사용될 때, 루프가 실행되는 동안 배열은 정적으로 유지되므로 문제가 발생하지 않습니다.


7

PHP foreach는 루프와 함께 사용할 수 있습니다 Indexed arrays, Associative arrays그리고Object 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


1
Object public variables잘못되었거나 잘못 오도 된 것입니다. 올바른 인터페이스 (예 : Traversible)가 없으면 어레이에서 객체를 사용할 수 없으며 foreach((array)$obj ...실제로 사용하면 더 이상 객체가 아닌 간단한 어레이로 작업하는 것입니다.
Christian
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.