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();
/* ... */
}
내부 클래스의 경우 본질적으로 Iterator
C 레벨 에서 인터페이스를 미러링하는 내부 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가이 요소를 가리키는 지 확인합니다. 그렇다면 다음 요소로 넘어갑니다.
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
다음과 같은 shenanigan을 수행하십시오. 루프 본문이 실행되기 전에 foreach
현재 요소에 대한 포인터와 해당 해시를 per-foreach로 백업합니다 HashPointer
. 루프 본문이 실행 된 후에도 IAP는 여전히 존재하는 경우이 요소로 다시 설정됩니다. 그러나 요소가 제거 된 경우 IAP가 현재 어디에 있든지 사용합니다. 이 계획은 주로 대부분의 작품이지만, 그로부터 얻을 수있는 이상한 행동이 많이 있으며, 그중 일부는 아래에서 설명합니다.
배열 복제
IAP는 current
쓰기 중 복사 시맨틱에서 수정 된 것으로 IAP의 변경 사항이 카운트되는 것처럼 배열의 가시적 인 기능 ( 기능 군을 통해 노출됨 )입니다. 불행히도 이것은 foreach
많은 경우 반복되는 배열을 복제해야 한다는 것을 의미합니다 . 정확한 조건은 다음과 같습니다.
- 배열이 참조가 아닙니다 (is_ref = 0). 참조 인 경우 변경 사항 이 전파 되는 것으로 간주 되므로 복제해서는 안됩니다.
- 배열의 참조 횟수는 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 변경 사항이 $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 모두 each
와 reset
참조로 함수이다. 는 $array
A가 들어 refcount=2
가 중복 될 수있다, 그래서 그것이 그들에게 전달 될 때. 따라서 foreach
별도의 배열에서 다시 작동합니다.
예 : current
foreach의 효과
다양한 복제 동작을 보여주는 좋은 방법 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이므로, 실제로 중복을 미리해야합니다. 따라서 $array
foreach에서 사용하는 배열은 처음부터 완전히 분리됩니다. 그렇기 때문에 루프 이전에 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 복원 메커니즘이 해시와 포인터 충돌로 인해 제거 된 요소와 동일한 것처럼 "보여"새 요소로 바로 이동했습니다. 더 이상 요소 해시에 의존하지 않기 때문에 더 이상 문제가되지 않습니다.