1735

이 접두사를 내가 무엇을 알고 있다고 말함으로써 접두사를 붙이겠습니다.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()et al.)foreach루프의 결과에 영향을 미칠 수 있습니까?


  • @DaveRandom 거기에PHP 내부태그와 함께 사용해야하지만 다른 5 개의 태그를 대체 할 것인지 결정해야합니다. - Michael Berkowski
  • COW처럼 보이지만 삭제 처리가 없습니다. - zb'
  • 처음에 나는 초짜 질문을 생각했다. 문서를 읽으십시오 ... hm, 명확하게 정의되지 않은 동작 « ;. 그런 다음 나는 완전한 질문을 읽고, 나는 그것을 말해야 만한다 : 나는 그것을 좋아한다. 테스트에 모든 노력을 기울여야합니다. 추신. 테스트 케이스 4와 5는 동일합니까? - knittl
  • 왜 배열 포인터가 만지는지 이해하는 이유에 대해 생각해보십시오 : PHP는 사용자가 현재 값에 대한 참조를 요청할 수 있기 때문에 원래 배열의 내부 배열 포인터를 복사와 함께 이동하고 재설정해야합니다foreach ($array as &$value)) - PHP는 복사본을 실제로 반복하더라도 원래 배열의 현재 위치를 알아야합니다. - Niko
  • @Sean : IMHO, PHP 문서는 핵심 언어 기능의 뉘앙스를 설명하는 데 실제로 상당히 나쁩니다. 하지만 아마 많은 특수한 경우가 언어로 구워지기 때문일 것입니다 ... - Oliver Charlesworth

7 답변


1449

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

내부 클래스의 경우 내부 API를 사용하여 실제 메소드 호출을 피할 수 있습니다.Iterator인터페이스는 C 레벨에있다.

배열과 일반 객체의 반복은 훨씬 더 복잡합니다. 우선, PHP에서 "배열"은 실제로 순서가 지정된 사전이며이 순서에 따라 가로 지르게됩니다 (삽입 순서와 일치합니다.sort). 이는 키의 자연 순서 (다른 언어의 목록이 자주 작동하는 방식) 또는 정의 된 순서가 전혀없는 (다른 언어의 사전이 자주 작동하는 방식) 반복에 반대합니다.

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

여태까지는 그런대로 잘됐다. 사전 반복은 너무 어려울 수 없습니다. 맞습니까? 문제는 반복 중에 배열 / 객체가 변경 될 수 있음을 알게되면서 시작됩니다. 이러한 일이 발생할 수있는 방법에는 여러 가지가 있습니다.

  • 참조를 기준으로foreach ($arr as &$v)그때$arr참조로 바뀌고 반복 중에 변경할 수 있습니다.
  • PHP 5에서는 값을 기준으로 반복하지만 배열이 미리 참조였던 경우에도 동일하게 적용됩니다.$ref =& $arr; foreach ($ref as $v)
  • 객체는 by-handle passing semantics를 가지고 있는데, 이는 대부분의 실제적인 목적을 위해 참조와 같이 동작한다는 것을 의미합니다. 따라서 객체는 반복되는 동안 항상 변경 될 수 있습니다.

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

이 문제를 해결하는 다양한 방법이 있습니다. PHP 5와 PHP 7은이 점에서 상당히 다르며 다음 두 가지 동작을 모두 설명하겠습니다. 요약하면 PHP 5의 접근 방식이 다소 바보스럽고 모든 종류의 이상한 사례로 이어지는 반면 PHP 7의 관여 된 접근 방식은 예측 가능하고 일관된 동작을 초래합니다.

마지막 예고로서, PHP는 참조 카운팅과 copy-on-write를 사용하여 메모리를 관리합니다. 즉, 값을 "복사"하면 이전 값을 재사용하고 참조 카운트 (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는 내부 배열 포인터가 하나 뿐인 두 개의 동시 루프를 지원하기 위해 다음과 같은 schenanigans를 수행합니다. 루프 본문이 실행되기 전에 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를 참조로 사용하면 (잠재적으로 복제 된) 배열이 참조로 바뀝니다.

다음 코드를 복사가 발생하는 예제로 생각하십시오.

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) 의미 체계를 위반합니다. 즉 COW는 refcount = 2 배열의 IAP를 수정하고 COW는 refcount에만 수정할 수 있음을 나타냅니다. = 1 값. 이 위반은 사용자가 볼 수있는 동작 변경을 유발합니다 (COW는 일반적으로 투명 함). 반복 된 배열의 IAP 변경 사항을 관찰 할 수 있기 때문에 - 그러나 배열의 첫 번째 비 IAP 수정까지. 대신에 세 가지 "유효한"옵션은 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. 이것이 반복되는 동안 수정을 보여주는 코드 샘플이 항상다음 것요소가 아니라 현재 요소입니다.

예 : 테스트 케이스

위에 설명 된 세 가지 측면은 foreach 구현의 특질에 대한 대부분의 완전한 인상을 제공해야하며 몇 가지 예를 논의 할 수 있습니다.

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

  • 테스트 케이스 1과 2$arrayrefcount = 1부터 시작하므로 foreach에 의해 중복되지 않습니다. refcount 만 증분됩니다. 루프 본문이 어레이를 수정하면 (그 시점에서 refcount = 2), 그 시점에서 복제가 발생합니다. Foreach는 수정되지 않은$array.

  • 테스트 케이스 3에서는 배열이 중복되지 않으므로 foreach가 IAP를 수정합니다.$array변하기 쉬운. 반복의 끝에서 IAP는 NULL입니다 (반복이 수행되었음을 의미).each돌아 오는 것으로 나타남false.

  • 테스트 케이스 4와 5 모두eachreset참조 함수입니다. 그만큼$array~을 가지고있다.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입니다. 참조에 의한 전달은 배열이 분리되어야 함을 의미하므로$arrayforeach 배열은 다를 것입니다. 네가 얻는 이유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참조로 iterated 때 is_ref = 1, 그래서 기본적으로 당신은 위와 같은 상황이 있습니다.

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

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

여기서 refcount는$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를 사용합니다!

그로 인한 또 다른 결과HashPointer백업 + 복원 메커니즘은 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는 요소 W 해시에 대한 포인터를 사용하여 여전히 존재하는지 판별합니다. 하지만 : 해시는 충돌이 있으며 포인터는 재사용 될 수 있습니다! 즉, 배열 키를 신중하게 선택하면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

Hashtable 반복자

여전히 기억하고 있다면 배열 반복의 주된 문제는 요소 반복 제거를 처리하는 방법이었습니다. PHP 5는이 목적을 위해 단일 내부 배열 포인터 (IAP)를 사용했습니다.이 배열은 여러 개의 동시 foreach 루프를 지원하기 위해 하나의 배열 포인터를 확장해야했기 때문에 다소 차선책이었습니다상호 작용reset()등등.

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

즉, foreach는 더 이상 IAP를 사용하지 않습니다.조금도. foreach 루프는 결과에 전혀 영향을 미치지 않습니다.current()그 자체의 행동은 다음과 같은 함수에 의해 결코 영향을받지 않을 것입니다.reset()기타

어레이 복제

PHP 5와 PHP 7의 또 다른 중요한 변화는 배열 중복과 관련이 있습니다. 이제는 IAP가 더 이상 사용되지 않으므로 by-value 배열 반복은 모든 경우에 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 */

이전에는 참조 배열의 by-value 반복이 특별한 경우였습니다. 이 경우 중복이 발생하지 않으므로 반복 과정에서 배열의 모든 수정 사항이 루프에 반영됩니다. PHP 7에서이 특별한 경우는 없어졌습니다 : 배열의 by-value 반복은항상루프가 진행되는 동안 수정 사항을 무시하고 원래 요소를 계속 사용하십시오.

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

$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 및 duplication 동작조차도 PHP 5와 PHP 7에서 동일합니다.)

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

  • 테스트 케이스 4와 5는 동일하게 유지됩니다.each()reset()foreach는 원래 배열을 사용하는 반면 IAP를 변경하기 전에 배열을 복제합니다. (배열이 공유되었다고하더라도 IAP 변경은 중요하지 않았을 것입니다.)

두 번째 예제 세트는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 복원 메커니즘은 해시 및 포인터 충돌로 인해 제거 된 요소와 동일한 것처럼 보였으므로 새 요소로 바로 건너 뛰었습니다. 우리가 더 이상 요소 해시에 의존하지 않으므로 더 이상 문제가되지 않습니다.


  • @ 바바 그렇습니다. 함수에 전달하는 것은$foo = $array루프 전에;) - NikiC
  • zval이 무엇인지 알 수없는 사용자는 Sara Goleman의blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
  • 사소한 수정 : 당신이 Bucket이라고 부르는 것은 해시 테이블에서 일반적으로 버킷이라고 불리는 것이 아닙니다. 일반적으로 버킷은 같은 해시 % 크기를 가진 항목 집합입니다. 당신은 일반적으로 항목이라고하는 것에 사용하는 것 같습니다. 링크 된 목록은 버킷에 있지만 항목에는 없습니다. - unbeli
  • @unbeli PHP에서 내부적으로 사용하는 용어를 사용하고 있습니다. 그만큼Buckets는 해시 충돌에 대한 이중 연결 목록의 일부이고 또한 주문에 대한 이중 연결 목록의 일부입니다. - NikiC
  • 위대한 anwser. 나는 네가 의미 한 것 같아.iterate($outerArr);하지iterate($arr);어딘가에. - niahoo

102

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

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

다음은 그 예입니다.

$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내보기에서. 그러나 반복자는 참조와 함께 루프 중에 만 살고 두 번 모두 무시됩니다. 이제는 모든 경우를 제외하고는 루프가 진행되는 동안 배열이 수정되는 반면,이 추가 참조는 살아 있음을 알 수 있습니다. 이것은 복제를 유발하고, 그것은 무슨 일이 일어나고 있는지를 설명합니다!

다음은이 copy-on-write 비헤이비어의 또 다른 부작용에 대한 훌륭한 기사입니다.PHP 삼항 연산자 : 빠르거나 없습니까?


  • 당신의 권리 인 것처럼 보입니다.codepad.org/OCjtvu8r귀하의 예와 다른 점 - 변경 키의 경우에만 값을 변경하면 복사되지 않습니다. - zb'
  • 이것은 실제로 위의 모든 행동을 설명하며, 전화를 통해 잘 설명 될 수 있습니다.each()첫 번째 테스트 케이스의 끝에서우리는보다원래 배열의 배열 포인터가 첫 번째 반복 중에 배열이 수정되었으므로 두 번째 요소를 가리키는 포인터입니다. 이것은 또한foreach예상치 않았던 루프의 코드 블록을 실행하기 전에 배열 포인터를 움직여서 결국 이렇게 할 것이라고 생각했을 것이다. 많은 감사합니다, 이것은 나를 위해 멋지게 정리합니다. - DaveRandom

39

함께 작업 할 때주의해야 할 점foreach():

에이)foreach~에서 작동합니다.예상 사본원래 배열의     그것은 foreach ()가 SHARED 데이터 저장을 가질 때까지 또는 저장하지 않을 때까지prospected copy~이다.     만들어지지 않았다.foreach Notes / 사용자 설명.

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


  • 귀하의 대답은 정확하지 않습니다.foreach배열의 잠재적 사본에서 작동하지만 필요하지 않으면 실제 사본을 만들지 않습니다. - linepogl
  • 코드를 통해 잠재적 사본이 언제 어떻게 작성되는지 시연하고 싶습니까? 내 코드는foreach배열을 100 % 복사하고 있습니다. 나는 열심히 알고 있습니다. 감사합니다. - sakhunzai
  • 배열 복사는 많은 비용이 듭니다. 둘 중 하나를 사용하여 100000 개의 요소가있는 배열을 반복하는 데 걸리는 시간을 계산해보십시오.for또는foreach. 실제 복사가 이루어지지 않기 때문에 두 가지 사이에 큰 차이가 나타나지 않습니다. - linepogl
  • 그런 다음 나는 거기 있다고 가정 할 것이다.SHARED data storage때까지 또는없이 예약copy-on-write,하지만 (내 코드 스 니펫에서) 그 두 세트가 항상 있다는 것을 분명히합니다.SENTINEL variables하나는original array및 기타foreach. 감사합니다. - sakhunzai
  • "prospected"? " 보호 "라는 의미입니까? - Peter Mortensen

26

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 ' 실제로 일하니?이 문제의 배경을 설명합니다.


  • 그럼 대답의 나머지 부분을 읽었습니까? foreach는 다른 시간에 루프 할 것인지 결정합니다.전에심지어 코드를 실행합니다. - Damir Kasipovic
  • 아니요, 배열은 수정되지만 " 너무 늦음 " foreach는 이미 "생각하다" 마지막 요소 (iteration의 시작 부분에 있음)에 있으며 더 이상 반복되지 않습니다. 두 번째 예제에서는 반복 시작 부분의 마지막 요소가 아니며 다음 반복의 시작 부분에서 다시 평가됩니다. 테스트 케이스를 준비하려고합니다. - Damir Kasipovic
  • @ AlmaDolxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509iterates 때 그것은 항상 다음 포인터로 설정됩니다. 따라서 마지막 반복에 도달하면 NULL 포인터를 통해 완료로 표시됩니다. 마지막 반복에서 키를 추가하면 foreach는이를 인식하지 못합니다. - bwoebi
  • @DKasipovic no. 없다.완료 (& 명확한그곳에서의 설명 (적어도 현재는 - 나는 틀릴 수도있다) - Alma Do
  • 실제로 그것은 @ AlmaDo가 자신의 논리를 이해하는 데있어 결함이있는 것 같습니다 ... 당신의 대답은 괜찮습니다. - bwoebi

11

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

각 반복에서 현재 요소의 값이 $ v에 할당되고 내부 요소

배열 포인터가 1만큼 앞당겨집니다 (다음 반복에서는 다음 요소를 보게 될 것입니다).

첫 번째 예제에 따라

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

$arrayforeach 실행에 따라 하나의 요소 만 가지므로 1에 할당$v포인터를 움직일 다른 요소가 없다.

그러나 두 번째 예 :

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

$array두 요소가 있으므로 $ array는 0 인덱스를 평가하고 포인터를 하나씩 이동합니다. 루프의 첫 번째 반복에 대해$array['baz']=3;참조로 통과.


9

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의 변경 사항이 원래 $ 집합의 멤버에게 적용됩니다. 참조로 $ 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 번만 실행되고 원본 $ 집합의 끝 부분에 다른 값을 추가 할 때마다 원래 $ 집합을 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는 루프의 배열 복사본을 작성하는 이유를 알 수 있습니다. 복사본이 생성되어 루프 구조의 구조에 의해서만 사용되는 경우 배열은 루프 실행 중에 정적으로 유지되므로 문제가 발생하지 않습니다.


5

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


  • Object public variables잘못되었거나 잘못 오인했습니다. 올바른 인터페이스가없는 배열에서는 객체를 사용할 수 없습니다 (예 : Traversible).foreach((array)$obj ...당신은 실제로 더 이상 객체가 아닌 단순한 배열로 작업하고 있습니다. - Christian

연결된 질문


관련된 질문

최근 질문