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()他))foreachループの結果に影響を与える可能性がありますか?


  • @DaveRandomがありますphp-internalstagこれはおそらくうまくいくはずですが、他の5つのタグのどれを置き換えるかを決めるのはあなたに任せます。 - Michael Berkowski
  • 削除ハンドルなしで、COWのように見えます - zb'
  • 最初は、初心者向けのもう1つの質問です。ドキュメントをお読みください…うーん、未定義の動作«。それから私は完全な質問を読み、そして私は言わなければなりません:私はそれが好きです。あなたはそれにかなりの努力を払い、すべてのテストケースを書きました。 ps。テストケース4と5は同じですか? - knittl
  • なぜ配列ポインタが触れられるのが理にかなっているのかについての考察:ユーザが現在の値への参照を要求するかもしれないので、PHPはコピーと共に元の配列の内部配列ポインタをリセットし移動する必要があります。foreach ($array as &$value)) - 実際にコピーを反復処理している場合でも、PHPは元の配列内の現在位置を知る必要があります。 - Niko
  • @ Sean:私見、PHPのドキュメントは、コアとなる言語機能の微妙な違いを説明するのが非常に苦手です。しかし、それはおそらく、非常に多くの特別な特別な場合が言語に焼き付けられているからです。 - Oliver Charlesworth

7 답변


1449

foreach3種類の異なる値に対する繰り返し処理をサポートします。

  • 配列
  • 通常のオブジェクト
  • 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();
    /* ... */
}

内部クラスの場合、実際のメソッド呼び出しは、基本的にIteratorCレベルのインタフェース。

配列とプレーンオブジェクトの繰り返しはかなり複雑です。まず第一に、PHPでは "配列"は実際には順序付けられた辞書であり、それらはこの順序に従ってトラバースされることに注意してください(これはあなたが好きではない限り挿入順序と一致します)。sort)これは、キーの自然な順序(他の言語のリストの動作方法)や、順序の定義がまったくない(他の言語の辞書の動作方法)こととは反対です。

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

ここまでは順調ですね。辞書をくりかえすことはそれほど難しくありませんね。問題は、配列/オブジェクトが反復中に変更される可能性があることに気付いたときに始まります。これが起こる可能性がある複数の方法があります:

  • を使用して参照で反復した場合foreach ($arr as &$v)それから$arrは参照に変換され、反復中に変更できます。
  • PHP 5では、値で繰り返しても同じことが当てはまりますが、配列はあらかじめ参照でした。$ref =& $arr; foreach ($ref as $v)
  • オブジェクトは、ハンドル渡しのセマンティクスを持ちます。これは、最も実用的な目的のためには、参照のように振舞うことを意味します。したがって、オブジェクトは反復中にいつでも変更できます。

繰り返しの間に変更を許可することに関する問題は、あなたが現在いる要素が削除される場合です。あなたが現在どの配列要素にいるのかを追跡するためにポインタを使うとしましょう。この要素が解放されると、ぶら下がりポインタが残ります(通常はセグメンテーション違反になります)。

この問題を解決するにはさまざまな方法があります。 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) {
        // ...
    }
}

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

配列の重複

IAPはアレイの目に見える機能です(currentそのようなIAPへの変更はコピーオンライトセマンティクスの下での修正としてカウントされます。残念ながら、これは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)、2か所で使用されます(refcount = 2)。この要件は残念であり、最適とは言えない実装の成果物です(ここでは反復中の変更の心配はありません。したがって、最初にIAPを使用する必要はありません)。

ここでrefcountを増やすことは無害に思えますが、コピーオンライト(COW)のセマンティクスに違反します:これはrefcount = 2の配列のIAPを変更しようとしているのに対し、COWはrefcountに対してのみ実行できることを示します= 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。これが、繰り返しの間に変更を示すコードサンプルが常にunsetを解除する理由です。現在の要素ではなく要素。

例:あなたのテストケース

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

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

  • テストケース1と2$arrayrefcount = 1から始まるので、foreachによって複製されることはありません。refcountだけが増分されます。その後ループ本体が(その時点でrefcount = 2を持つ)配列を変更すると、その時点で重複が発生します。 Foreachは変更されていないコピーを作成し続けます。$array

  • テストケース3では、配列は二重化されていないので、foreachはIAPを変更します。$array変数。反復の終わりに、IAPはNULLになります(反復が終了したことを意味します)。each戻ることで示すfalse

  • テストケース4と5の両方でeachそしてreset参照関数です。の$arrayがありますrefcount=2それが彼らに渡されるとき、それは複製されなければなりません。などforeach別の配列で作業します。

例:の効果current前に

さまざまな複製動作を示す良い方法は、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配列は異なります。あなたが得る理由2の代わりに1また上記で言及されている: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は同じ配列で動作します。あなたはまだその方法のために、まだ1人ずつの振る舞いを見ますforeachポインタを進めます。

参照による繰り返しを実行した場合も同じ動作になります。

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

ここでのrefcount$arrayループが開始されると2になるので、実際には複製を事前に行わなければなりません。このように$arrayそしてforeachによって使用される配列は最初から完全に分離されます。それが、ループの前であればどこでも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)elementが原因で出力から欠落しています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は、要素とそのハッシュへのポインタを使用して、それがまだ存在するかどうかを判断します。しかし、ハッシュは衝突を起こし、ポインタは再利用できます。これは、配列キーを慎重に選択すれば、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

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

それでも覚えているなら、配列反復の主な問題は、反復の途中で要素の削除を処理する方法です。 PHP 5では、この目的のために単一の内部配列ポインタ(IAP)を使用していました。これは、複数の同時foreachループをサポートするために1つの配列ポインタを引き伸ばす必要があったためです。そしてとの対話reset()その上になど。

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

これはforeachがもはやIAPを使わないことを意味しますまったく。 foreachループは次の結果にまったく影響を与えません。current()などとそれ自身の振る舞いは、次のような関数の影響を受けません。reset()

配列の重複

PHP 5とPHP 7の間のもう1つの重要な変更は、配列の複製に関するものです。 IAPが使用されなくなったため、値による配列の反復では、すべての場合において(配列の複製ではなく)参照カウントの増分のみが行われます。 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は同じままです。each()そしてreset()foreachは元の配列を使用しながら、IAPを変更する前に配列を複製します。 (アレイが共有されていても、IAPの変更は重要ではありませんでした。)

例の2番目のセットは、の動作に関連していましたcurrent()異なった参照/ 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復元メカニズムは、削除された要素と同じように見えたため(ハッシュとポインタが衝突したため)、新しい要素にジャンプしました。要素ハッシュに頼らなくなったので、これはもはや問題になりません。


  • @ババします。それを関数に渡すことはすることと同じです。$foo = $arrayループの前;) - NikiC
  • zvalが何であるかがわからない場合は、Sara Golemanを参照してください。blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
  • 細かい修正:あなたがBucketと呼んでいるものはハッシュテーブルで通常Bucketと呼ばれているものではありません。通常、Bucketは同じハッシュサイズ%のエントリの集合です。通常エントリと呼ばれるものにそれを使用しているようです。リンクリストはバケットにはありませんが、エントリにはありません。 - unbeli
  • @unbeli私はPHPの内部で使用されている用語を使用しています。のBucketsはハッシュ衝突のための二重リンクリストの一部であり、順序のための二重リンクリストの一部でもある;) - NikiC
  • 素晴らしい前菜私はあなたが言ったことだと思うiterate($outerArr);ではなくiterate($arr);どこかに。 - niahoo

102

例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を除くすべてのケースで、配列がループ中に変更されていることがわかりますが、この追加の参照は有効です。これはクローンを引き起こします、そしてそれはここで何が起こっているのかを説明します!

これが、このコピーオンライト動作の別の副作用に関する優れた記事です。PHPの三項演算子:高速かどうか


  • あなたの権利のようです、私はそれを実証するいくつかの例を作りました:codepad.org/OCjtvu8rあなたの例との1つの違い - それはあなたが値を変更した場合はコピーされず、キーを変更した場合にのみコピーされます。 - zb'
  • これは実際に上記のすべての動作を説明しています。each()最初のテストケースの最後に、私たちは見る最初の繰り返しで配列が変更されたため、元の配列の配列ポインタが2番目の要素を指すこと。これはまたそれを実証するようですforeachループのコードブロックを実行する前に配列ポインタを移動します。これは予期していませんでしたが、最後にこれを実行すると考えていたはずです。どうもありがとう、これは私にとってそれをきれいに片付けます。 - DaveRandom

39

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

a)foreachに取り組んで見込コピー元の配列     つまり、foreach()は、以下の条件が満たされるまで、またはそうでない限り、SHAREDデータストレージを持ちます。prospected copyです     作成されていませんコメント/ユーザーコメント

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しかし、(私のコードスニペットから)2つのセットが常にあることは明らかですSENTINEL variablesのためのものoriginal arrayその他foreach。理にかなっているありがとう - sakhunzai
  • 「予想」 「保護」という意味ですか。 - Peter Mortensen

26

PHP 7に関する注意

人気が高まっているのでこの回答を更新するには:この回答は、PHP 7以降では適用されなくなりました。後方互換性のない変更"、PHP 7ではforeachは配列のコピーを処理するので、配列自体の変更はforeachループに反映されません。詳細はリンク先をご覧ください。

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

最初の形式は、array_expressionによって指定された配列をループ処理します。それぞれの   繰り返し、現在の要素の値は$ valueに代入され、   内部配列ポインタは1つ進められます(したがって、次に進みます)。   繰り返し、次の要素を見ます。

したがって、最初の例では配列内に要素が1つしかなく、ポインタを移動したときに次の要素は存在しません。新しい要素を追加した後は、最後の要素として既に決定したためです。

2番目の例では、2つの要素から始めますが、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は既に「と思う」のでそれは最後の要素(それは繰り返しの開始点にある)にあり、もうループしないということです。 2番目の例では、反復の開始時に最後の要素ではなく、次の反復の開始時に再び評価されます。テストケースを準備しようとしています。 - Damir Kasipovic
  • @ AlmaDoを見てくださいlxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509反復するときは常に次のポインタに設定されます。そのため、最後の繰り返しに達すると、(NULLポインタを介して)終了マークが付けられます。その後、最後の繰り返しでキーを追加しても、foreachはそれに気付きません。 - bwoebi
  • @ Dカシポビッチ号ありません完了(& A)クリアそこに説明(少なくとも今のところ - 私は間違っているかもしれません) - Alma Do
  • 実のところ、@ AlmaDoは自分の論理を理解するのに欠陥があるようです…あなたの答えは大丈夫です。 - bwoebi

11

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は0のインデックスを評価し、ポインタを1つ移動します。ループの最初の繰り返しに対して、追加$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に加えられた変更は元の$ setのメンバーに行われます。参照によって$ 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回しか実行されず、そのたびに元の$ setの最後に別の値が追加され、元の$ setの要素数は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 arraysAssociative 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


  • Object public variables間違っているか、せいぜい誤解を招くようです。正しいインターフェース(例:Traversible)がなければ、オブジェクトを配列内で使用することはできません。foreach((array)$obj ...実際には、オブジェクトではなく単純な配列を使って作業しています。 - Christian

リンクされた質問


関連する質問

最近の質問