配列 - php foreach=>




PHPのforeachは実際にどのように機能しますか? (5)

foreach()使用する際に注意すべき点がいくつかあります。

a) foreachは元の配列の予想されるコピーで動作します。 これは、 prospected copymanual作成されない限り、foreach()がSHAREDデータストレージを持つことを意味しmanual

b) 予想されるコピーをトリガするのは何ですか? 予想されるコピーは、 copy-on-writeのポリシーに基づいて作成されcopy-on-write 。つまり、foreach()に渡された配列が変更されると、元の配列のクローンが作成されます。

c)元の配列とforeach()イテレータはDISTINCT SENTINEL VARIABLESを持ちDISTINCT SENTINEL VARIABLESつまり元の配列とforeachのためのものです。 以下のテストコードを参照してください。 SPLIterators 、および配列イテレータ

どのようにPHPの 'foreach'ループで値がリセットされていることを確認するには? あなたの質問のケース(3,4,5)に対処します。

次の例は、each()とreset()がforeach()イテレータのSENTINEL変数(for example, the current index variable)に影響しないこと(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

私は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()を調整する関数を使用してループの結果に影響を与える可能性のある状況はありますか?

foreachは3つの異なる種類の値に対して反復をサポートします:

  • 配列
  • 通常のオブジェクト
  • Traversableオブジェクト

以下では、異なるケースで反復がどのように機能するかを正確に説明しようとします。 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インターフェイスをIteratorだけの内部APIを使用することによって回避されます。

配列と単純なオブジェクトの反復ははるかに複雑です。 まず第一に、PHPでは "配列"は本当に順序付けされた辞書であり、この順序に従ってsortようなsort使用しない限り挿入順序に一致しsort )。 これは、キーの自然な順序(他の言語のリストがしばしばどのように動作するか)や定義されていない順序(他の言語の辞書がしばしばうまくいくかどうか)を反復するのとは対照的です。

オブジェクトのプロパティは、別の(順序付けられた)ディクショナリのプロパティ名とその値のマッピング、およびある種の可視性の処理として見ることができるため、オブジェクトにも適用されます。 大多数の場合、オブジェクトのプロパティは実際にはこのように非効率的な方法で格納されません。 しかし、オブジェクトの反復処理を開始すると、通常使用されるパック表現は実際の辞書に変換されます。 その時点で、プレーンなオブジェクトの反復は配列の反復と非常によく似ています(ここで私は単純なオブジェクトの反復について議論していません)。

ここまでは順調ですね。 辞書を繰り返すことはそれほど難しいことではないでしょうか? 問題は、反復処理中に配列/オブジェクトが変更されることがわかったときに始まります。 これには複数の方法があります。

  • foreach ($arr as &$v)を使用して参照によって反復すると、 $arrは参照に変換され、反復中に変更することができます。
  • PHP 5では値を反復しても同じことが適用されますが、配列はあらかじめ参照されています: $ref =& $arr; foreach ($ref as $v) $ref =& $arr; foreach ($ref as $v)
  • オブジェクトにはセマンティクスが渡されます。実用的な意味では、それらは参照のように動作します。 したがって、オブジェクトは反復中に常に変更することができます。

反復中に変更を許可する問題は、あなたが現在いる要素が削除された場合です。 どの配列要素が現在あるかを追跡するためにポインタを使用するとします。 この要素が解放されると、ぶら下がったポインタが残されます(通常、segfaultが発生します)。

この問題を解決するさまざまな方法があります。 PHP 5とPHP 7はこの点で大きく異なります。次に、両方の動作について説明します。 要約すると、PHP 5のアプローチはむしろばかばかしく、あらゆる種類の奇妙なエッジケースの問題につながります。一方、PHP 7のより複雑なアプローチは、より予測可能で一貫した動作をもたらします。

最後の暫定として、PHPはリファレンスカウントとコピーオンライトを使用してメモリを管理することに注意してください。 これは、値を "コピー"すると、実際には古い値を再利用し、参照カウント(refcount)を増やすことを意味します。 いったん何らかの変更を行うと、実際のコピー(「複製」と呼ばれます)が実行されます。 あなたはこのトピックに関するより広範な紹介のために嘘をついています

PHP 5

内部配列ポインタとHashPointer

PHP 5の配列には、変更を適切にサポートする専用の「内部配列ポインタ」(IAP)が1つあります。要素が削除されるたびに、IAPがこの要素を指しているかどうかをチェックします。 そうであれば、代わりに次の要素に進む。

foreachはIAPを使用していますが、追加の複雑さがあります:1つのIAPしかありませんが、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) {
        // ...
    }
}

foreachは、内部配列ポインタを1つしか持たない2つの同時ループをサポートするために、次のようなschenanigansを実行します。ループ本体が実行される前に、foreachは現在の要素へのポインタとそのハッシュをforeach HashPointerごとに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$arr IAP変更が$outerArr漏れないように複製されます。 上記の条件では、配列は参照(is_ref = 0)ではなく、2つの場所(refcount = 2)で使用されます。 この要件は不幸なことであり、準最適な実装の成果物です(ここでの反復処理中に変更の懸念がないため、最初はIAPを使用する必要はありません)。

refcount = 2の配列のIAPを変更することを意味しますが、COWはrefcountに対してのみ変更を実行するように指示しています(つまり、refcount = 2の配列のIAPを変更しようとしています) = 1の値。 この違反は、反復配列のIAP変更が観測可能になるため(ただし、COWは通常透過的ですが)、ユーザーが視覚的に行動を変更できるようになります。ただし、配列の最初の非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();
}

つまり、ループ本体が実行される前に、配列ポインタはすでに前方移動しています。 これは、ループ本体が要素$iで動作している間に、IAPがすでに要素$i+1ことを意味します。 これは、反復中に変更を示すコードサンプルが、現在の要素ではなく、常に次の要素の設定を解除する理由です。

例:テストケース

上で説明した3つの側面は、foreach実装の特異性についてのほとんど完全な印象を提供するはずであり、いくつかの例について議論することに移ります。

テストケースの動作は、この時点で簡単に説明できます。

  • テストケース1と2では、 $arrayはrefcount = 1で始まるので、foreachによって複写されません。refcountだけがインクリメントされます。 ループ本体がその後(その時点でrefcount = 2を持つ)配列を変更すると、その時点で複製が実行されます。 Foreachは$array変更されていないコピーで作業を続けます。

  • テストケース3では、配列が重複しないので、foreachは$array変数のIAPを変更します。 反復の終わりに、IAPはNULL(繰り返しを意味する)であり、 eachfalseを返すことを示しfalse

  • テストケース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)であることを知っておくべきです。 すべてのby-refであるnextような他のすべての関数とうまくやり遂げるためには、それが必要です。 参照渡しは、配列を分離しなければならないことを意味します。したがって、 $arrayとforeach-arrayは異なります。 あなたが2代わりに2を得る理由は、上記のとおりです: foreachは、ユーザコードを実行する前に配列ポインタを進めます。 コードが最初の要素にあるとしても、foreachは既にポインタを2番目の要素に進めました。

今すぐ小さな修正を試してみましょう:

$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にするということです。したがって、基本的には上記と同じ状況になります。

もう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位置(それが何であっても)が代わりに使用されます。 上記の例ではまさにそのケースです:外側のループの現在の要素は削除されているので、内側のループで終了してマークされている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'と同じハッシュを持ち、アロケータは同じメモリ位置を再利用して要素を格納します。 したがって、foreachは新たに挿入された要素に直接ジャンプして終了し、ループを短く切断します。

ループ中に反復されたエンティティを代入する

私が言いたい最後の奇妙なケースは、ループ中に反復されたエンティティをPHPで置き換えることができるということです。 したがって、1つの配列を反復処理してから、途中で別の配列に置き換えることができます。 または、配列の繰り返しを開始し、それをオブジェクトに置き換えます。

$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)を1つ使用していましたが、複数のforeachループ同時にサポートするために配列ポインタを1つ増やし、 reset()

PHP 7は異なるアプローチを採用しています。つまり、任意の量の外部の安全なハッシュテーブルイテレータの作成をサポートしています。 これらのイテレータは、配列に登録する必要があります。その点から、IAPと同じセマンティクスを持ちます。配列要素が削除された場合、その要素を指すすべてのハッシュテーブルイテレータは次の要素に進められます。

つまり、foreachはIAP 使用しなくなります。 foreachループはcurrent()などの結果にはまったく影響を与えません。それ自身の動作はreset()などの関数によって決して影響を受けません。

配列の複製

PHP 5とPHP 7の間のもう1つの重要な変更点は、配列の重複に関連しています。 IAPはもはや使用されていないので、バイナリ配列の反復はすべてのケースでrefcountインクリメント(配列の複製ではなく)だけを行います。 foreachループ中に配列が変更された場合、その時点で複製が発生し(copy-on-writeに従って)、foreachは古い配列で作業を続けます。

ほとんどの場合、この変更は透過的で、パフォーマンスの向上以外の効果はありません。 しかしながら、それが異なる挙動を生じる1つの機会、すなわちアレイがあらかじめ基準であった場合がある:

$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とduplicationの動作もPHP 5とPHP 7ではまったく同じです)。

  • テストケース3が変更されました。ForeachはIAPを使用しないため、 each()はループの影響を受けません。 前後に同じ出力が表示されます。

  • テストケース4と5は同じままです:foreachは元の配列を使用していますが、 each()reset()はIAPを変更する前に配列を複製します。 (配列が共有されていても、IAPの変更は重要ではないでしょう)。

2番目の例は、異なる参照/再構成設定の下での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要素と同じように "見た目"になったため、新しい要素に右にジャンプしました。 何も要素ハッシュに依存しなくなったので、これはもはや問題ではありません。


PHPマニュアルで提供されているドキュメントに従ってください。

各反復で、現在の要素の値が$ vに割り当てられ、内部
配列ポインタが1つ進められます(次の反復では、次の要素を参照します)。

あなたの最初の例によると:

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

$array単一の要素しか持たないので、foreachの実行ごとに1つの要素に割り当てられ$v、ポインタを移動する要素はありません

しかし、あなたの2番目の例では:

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

$array2つの要素を持つので、$ arrayはゼロインデックスを評価し、ポインタを1つだけ移動します。ループの最初の繰り返しに対して、$array['baz']=3;参照渡しとして追加されました。


例3では、配列を変更しません。 その他のすべての例では、内容または内部配列ポインタを変更します。 これは、代入演算子のセマンティクスのためにPHP配列に関して重要です。

PHPの配列の代入演算子は、遅延クローンのように機能します。 配列を含む別の変数に1つの変数を代入すると、ほとんどの言語とは異なり、配列が複製されます。 ただし、必要な場合を除いて実際のクローニングは行われません。 これは、いずれかの変数が変更されたとき(コピーオンライト)にのみクローンが実行されることを意味します。

次に例を示します。

$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 3者演算子:高速かどうか?


素晴らしい質問です。経験豊富な開発者でさえ、多くの開発者が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はループの実行と$ itemの代入のためにコピーされた配列のみを使用しています。このため、上記のループは3回だけ実行され、オリジナルの$セットの最後に別の値を追加するたびに、元の$セットに6つの要素が残されますが、決して無限ループに入ることはありません。

しかし、前に述べたように、$ itemを参照として使用した場合はどうなりますか?上記のテストに1文字追加:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $set[] = ucfirst($item);
}
print_r($set);

結果は無限ループになります。これは実際に無限ループであることに注意してください。スクリプトを自分で削除するか、OSがメモリ不足になるのを待つ必要があります。スクリプトに次の行を追加して、PHPのメモリが非常に不足するようにしました。これらの無限ループテストを実行する場合は、同じことをお勧めします。

ini_set("memory_limit","1M");

したがって、前の無限ループの例では、ループする配列のコピーを作成するためにPHPが書かれた理由がわかります。コピーが作成され、ループ構造自体の構造によってのみ使用される場合、配列はループの実行中は静的なままであるため、問題に遭遇することはありません。





php-internals