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




php foreach문 (6)

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는 루프의 배열 복사본을 작성하는 이유를 알 수 있습니다. 복사본이 생성되어 루프 구조의 구조에 의해서만 사용되는 경우 배열은 루프 실행 중에 정적으로 유지되므로 문제가 발생하지 않습니다.

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

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

manual 보면 다음과 같은 내용을 알 수 있습니다.

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

Right ... 이것은 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()each() 를 조정하는 함수를 사용하여 루프의 결과에 영향을 줄 수있는 상황이 있습니까?

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에서 "배열"은 실제로 순서가 지정된 사전이며,이 순서 ( sortsort 것을 사용하지 않는 한 삽입 순서와 일치 함)에 따라 가로 지르게됩니다. 이는 키의 자연 순서 (다른 언어의 목록이 자주 작동하는 방식) 또는 정의 된 순서가 전혀없는 (다른 언어의 사전이 자주 작동하는 방식) 반복에 반대합니다.

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

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

  • foreach ($arr as &$v) 를 사용하여 참조로 반복하면 $arr 이 참조로 바뀌고 반복 중에 변경할 수 있습니다.
  • PHP 5에서는 값에 따라 반복을 수행하더라도 동일하게 적용되지만 배열은 미리 참조가되었습니다 : $ref =& $arr; foreach ($ref as $v) $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는 copy-on-write 의미 체계 하에서의 수정과 같은 IAP 카운트 변경과 같은 어레이의 가시적 인 기능입니다 ( current 기능 패밀리를 통해 노출됨). 이것은 불행히도 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);

$arr 에 대한 IAP 변경이 $outerArr 누출되지 않도록 $arr 이 복제됩니다. 위의 조건에서 배열은 참조 (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에서 $array 는 refcount = 1로 시작하므로 foreach에 의해 중복되지 않습니다. refcount 만 증분됩니다. 루프 본문이 어레이를 수정하면 (그 시점에서 refcount = 2), 그 시점에서 복제가 발생합니다. Foreach는 $array 의 수정되지 않은 복사본에서 계속 작업 할 것입니다.

  • 테스트 케이스 3에서는 배열이 중복되지 않으므로 foreach는 $array 변수의 IAP를 수정합니다. 반복의 끝에서 IAP는 NULL (반복을 의미 함)이며, each false 를 반환 each 나타냅니다.

  • 테스트 케이스 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 함수 (실제로 : prefer-ref)임을 알아야합니다. 모두와 같이 next 과 같은 다른 모든 기능과 함께 멋지게 플레이해야합니다. 참조에 의한 전달은 배열이 분리되어야 함을 의미하므로 $array 와 foreach 배열은 달라집니다. 위에서 1 대신 2 를 얻는 이유는 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 가 포인터를 앞으로 나아가는 방식 때문에 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입니다. 그래서 한 번만 실제로 복제본을해야합니다. 따라서 foreach가 사용하는 $array 와 배열은 처음부터 완전히 분리됩니다. 이것이 루프 이전의 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 위치 (그것이 무엇이든지간에)가 대신 사용됩니다. 위의 예제에서 이것은 정확하게 적용됩니다 : 바깥 쪽 루프의 현재 요소는 remove 였으므로 이미 내부 루프에 의해 끝난 것으로 표시된 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

그러나 그러한 예는 여전히 정직합니다. 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' 와 동일한 해시를 '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는 동일하게 유지됩니다. foreach는 원래 배열을 사용하는 반면 each()reset() 은 IAP를 변경하기 전에 배열을 복제합니다. (배열이 공유되었다고하더라도 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 복원 메커니즘이 새로운 요소로 바로 건너 뛰었습니다. 이는 해시 및 포인터 충돌로 인해 remove 요소와 동일하게 "보았 기"때문입니다. 우리가 더 이상 요소 해시에 의존하지 않으므로 더 이상 문제가되지 않습니다.


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

a) foreach 는 원래 배열의 예상 복사본 에서 작동합니다. 이것은 예측 된 prospected copymanual 생성되지 않을 때까지 foreach ()가 SHARED 데이터 저장소를 가질 것임을 의미합니다.

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

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

question 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

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 인덱스를 평가하고 포인터를 1만큼 이동합니다. 루프의 첫 번째 반복의 경우 $array['baz']=3;참조로 전달됩니다.


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


예제 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 삼자 연산자 : 빠르거나 없습니까?





php-internals