2186

私はそれが何で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
4

7 に答える 7

1773

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

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 は参照カウントとコピー オン ライトを使用してメモリを管理することに注意してください。これは、値を「コピー」すると、実際には古い値を再利用して参照カウント (refcount) をインクリメントするだけであることを意味します。何らかの変更を行って初めて、実際のコピー (「複製」と呼ばれます) が作成されます。このトピックに関するより広範な紹介については、You're being lied toを参照してください。

PHP5

内部配列ポインタと HashPointer

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

foreachは IAP を利用しますが、さらに複雑な問題があります。IAP は 1 つしかありませんが、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) {
        // ...
    }
}

内部配列ポインターが 1 つだけの 2 つの同時ループをサポートするためにforeach、次の悪ふざけを実行します: ループ本体が実行される前foreachに、現在の要素へのポインターとそのハッシュを foreach ごとにバックアップしますHashPointer。ループ本体が実行された後、IAP はこの要素がまだ存在する場合に設定されます。ただし、要素が削除されている場合は、IAP が現在ある場所をそのまま使用します。このスキームはほとんどの場合、ある程度機能しますが、そこから抜け出すことができる多くの奇妙な動作があります。その一部を以下に示します。

配列の複製

IAP は配列の目に見える機能です (current関数ファミリーを通じて公開されます)。IAP へのそのような変更は、コピー オン ライト セマンティクスの下での変更としてカウントされます。残念ながら、これforeachは、多くの場合、反復処理中の配列を複製せざるを得ないことを意味します。正確な条件は次のとおりです。

  1. 配列は参照ではありません (is_ref=0)。それが参照である場合、それへの変更は伝播するはずなので、複製しないでください。
  2. 配列の refcount>1 です。が 1 の場合refcount、配列は共有されておらず、自由に直接変更できます。

配列が複製されていない場合 (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)、2 つの場所で使用されます (refcount=2)。この要件は残念なことであり、次善の実装のアーティファクトです (ここでは反復中の変更の懸念がないため、そもそも IAP を実際に使用する必要はありません)。

(*)refcountヒアのインクリメントは無害に聞こえますが、コピー オン ライト (COW) のセマンティクスに違反します: これは、refcount=2 配列の IAP を変更することを意味しますが、COW は変更が refcount= でのみ実行できることを指示します。 1 つの値。この違反により、(COW は通常は透過的ですが) ユーザーに見える動作の変更が発生します。これは、反復配列での IAP の変更が観察可能になるためです。ただし、配列での最初の非 IAP 変更までしか観察できません。代わりに、3 つの「有効な」オプションは、a) 常に複製する、b) をインクリメントせずrefcount、反復配列をループ内で任意に変更できるようにする、c) IAP をまったく使用しない (PHP 7 ソリューション)。

順位上げ順

以下のコード サンプルを正しく理解するために知っておく必要がある最後の実装の詳細が 1 つあります。一部のデータ構造をループする「通常の」方法は、擬似コードで次のようになります。

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

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

例: テスト ケース

上記の 3 つの側面から、foreach実装の特異性についてほぼ完全な印象が得られるはずです。次に、いくつかの例について説明します。

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

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

  • テスト ケース 3 では、配列が複製されていないため、変数foreachの IAP が変更されます。$array反復の最後に、IAP は NULL (反復が完了したことを意味します) であり、これeachは を返すことによって示されfalseます。

  • テスト ケース 4 と 5 では、eachresetの両方が参照関数です。には渡されるときに があるため、複製する必要があります$arrayrefcount=2そのためforeach、別の配列で再び作業します。

例: currentin foreachの効果

さまざまな複製の動作を示す良い方法は、ループcurrent()内の関数の動作を観察すること です。foreach次の例を検討してください。

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

current()ここでは、配列を変更しませんが、それが参照参照関数 (実際には、prefer-ref) であることを知っておく必要があります。これは、すべて参照による他のすべての関数とうまく連携するために必要ですnext。参照渡しは、配列を分離する必要があることを意味するため$array、 と はforeach-array異なります。2代わりに取得する理由1も上記のとおりです。ユーザーコードを実行した後ではなく、実行するforeachに配列ポインターを進めます。したがって、コードは最初の要素にありますが、既にポインターを 2 番目の要素に進めています。foreach

ここで、小さな変更を試してみましょう。

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

ここでは is_ref=1 の場合があるため、配列はコピーされません (上記と同様)。current()しかし、これが参照であるため、by-ref関数に渡すときに配列を複製する必要はなくなりました。したがって、同じ配列で作業しますcurrent()。ただし、がポインターを進めるforeach方法により、1 つずれる動作が引き続き表示されます。foreach

参照による反復を行う場合、同じ動作が得られます。

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

ここで重要な部分は、$array参照によって反復されるときに foreach が is_ref=1 を作成することです。そのため、基本的に上記と同じ状況になります。

別の小さなバリエーションとして、今回は配列を別の変数に割り当てます。

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

ここでは、ループが開始されたときの refcount$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バックアップと復元のメカニズムのもう 1 つの結果は、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'削除された element と同じハッシュがあり'EzFY'、アロケータがたまたま同じメモリ位置を再利用して要素を格納するということです。したがって、 foreach は、新しく挿入された要素に直接ジャンプすることになり、ループをショートカットします。

ループ中の反復エンティティの置換

最後に言及したい奇妙なケースの 1 つは、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 は他のエンティティを最初から反復し始めます。

PHP7

ハッシュテーブル イテレータ

覚えていれば、配列反復の主な問題は反復途中の要素の削除を処理する方法でした。PHP 5 は、この目的のために単一の内部配列ポインター (IAP) を使用していましたが、これはやや最適ではありませんでした。1 つの配列ポインターを拡張して、複数の foreach ループreset()同時に実行し、その上でその他の操作をサポートする必要があったからです。

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

これは、foreachが IAPをまったく使用しないことを意味します。foreachループは etc の結果にまったく影響を与えず、ループ自体の動作はetccurrent()のような関数によって影響を受けることはありません 。reset()

配列の複製

PHP 5 と PHP 7 の間のもう 1 つの重要な変更点は、配列の複製に関するものです。IAP が使用されなくなったため、値による配列反復はrefcount、すべての場合で (配列の複製ではなく) 増分のみを行います。ループ中に配列が変更された場合、foreachその時点で重複が発生し (コピーオンライトに従って) 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と重複の動作は PHP 5 と PHP 7 でまったく同じです)。

  • テスト ケース 3 の変更: ForeachIAP を使用しなくなったためeach()、ループの影響を受けません。前後で同じ出力になります。

  • テスト ケース 4 と 5 は同じままです。元のアレイを使用しながら、IAP を変更する前each()reset()アレイを複製します。foreach(アレイが共有されていたとしても、IAP の変更は問題ではありませんでした。)

current()2 番目の例は、さまざまな構成での の動作に関連していました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 を介した両方のループのクロスコンタミネーションがなくなったためです。

現在修正されているもう 1 つの奇妙なエッジ ケースは、たまたま同じハッシュを持つ要素を削除および追加したときに得られる奇妙な効果です。

$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 復元メカニズムが新しい要素にジャンプしていました。これは、削除された要素と同じように「見えた」ためです (ハッシュとポインターが衝突するため)。要素ハッシュに依存しなくなったため、これはもはや問題ではありません。

于 2013-02-13T13:21:40.760 に答える
123

例 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 を除くすべてのケースで、この余分な参照が生きている間に、ループ中に配列が変更されることがわかります。これによりクローンがトリガーされ、ここで何が起こっているかが説明されます!

このコピー オン ライト動作の別の副作用については、次の優れた記事を参照してください: The PHP Ternary Operator: Fast or not?

于 2012-04-07T20:43:49.463 に答える
55

を使用する際の注意点foreach():

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

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

c) 元の配列とforeach()イテレータにはDISTINCT SENTINEL VARIABLES、つまり、元の配列用の 1 つと 用の 1 つがありforeachます。以下のテストコードを参照してください。SPLIterators、およびArray Iterator

スタック オーバーフローの質問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
于 2012-04-07T21:03:00.483 に答える
39

PHP 7 に関する注意事項

ある程度の人気を得たので、この回答を更新するには: この回答はPHP 7 の時点では適用されなくなりました。 foreach ループには反映されません。詳細はリンク先で。

説明 ( php.netからの引用):

最初の形式は、array_expression で指定された配列をループします。各反復で、現在の要素の値が $value に割り当てられ、内部配列ポインターが 1 つ進められます (次の反復では、次の要素を見ることになります)。

したがって、最初の例では、配列に要素が1つしかなく、ポインターが移動すると次の要素が存在しないため、新しい要素を追加した後 foreach は、それが最後の要素であるとすでに「決定」しているため終了します。

2 番目の例では、2 つの要素で開始し、foreach ループが最後の要素ではないため、次の反復で配列を評価し、配列に新しい要素があることを認識します。

これはすべて、ドキュメントの説明のOn each iteration部分の結果であると思います。これは、おそらく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既に配列の最後の要素にあるときに変更したため、もうループしないことを「決定」し、新しい要素を追加したにもかかわらず、追加したのが「遅すぎた」ため、ループされませんでした。

詳細な説明はHow does PHP 'foreach' really work?で読むことができます。これは、この動作の背後にある内部構造を説明しています。

于 2014-04-15T08:46:20.153 に答える
16

PHPマニュアルが提供するドキュメントに従って。

反復ごとに、現在の要素の値が $v に割り当てられ、内部
配列ポインターが 1 つ進められます (次の反復では、次の要素を見ることになります)。

最初の例のように:

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

$array要素が 1 つしかないため、foreach の実行に従って、1 つの割り当て先が$vあり、ポインターを移動する他の要素はありません

しかし、2番目の例では:

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

$array$array には 2 つの要素があるため、ここで $array はゼロのインデックスを評価し、ポインターを 1 つ移動します。ループの最初の反復では、$array['baz']=3;参照渡しとして追加されます。

于 2014-04-15T09:32:39.960 に答える