PHPの「foreach」は実際にどのように機能しますか?


2018

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

5
@DaveRandom php-internalsタグがあるはずですが、他の5つのタグのどれを置き換えるかはあなたに任せます。
Michael Berkowski

5
削除ハンドルなしのCOWのように見える
zb '

149
最初、私は»ああ、もう1つの初心者の質問だと思いました。ドキュメントを読んでください…ええと、明らかに未定義の動作«。それから私は完全な質問を読み、私は言わなければなりません:私はそれが好きです。あなたはそれにかなりの努力を払い、すべてのテストケースを書きました。ps。テストケース4と5は同じですか?
knittl 2012

21
それはメイクセンスを行い理由についてだけ考えている配列ポインタを取得触れ:PHPは、ユーザーが現在の値への参照を求めることができるので、リセットして、コピーと一緒に元の配列の内部配列ポインタを移動する必要があります(foreach ($array as &$value)) - PHPは実際にコピーを繰り返し処理している場合でも、元の配列の現在の位置を知る必要があります。
Niko

4
@Sean:私見、PHPのドキュメントは、コア言語機能のニュアンスを説明するのが非常に悪いです。しかし、それはおそらく、非常に多くの特別なケースが言語に組み込まれているためです...
オリバーチャールズワース2013

回答:


1660

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)をインクリメントするだけです。ある種の変更を実行すると、実際のコピー(「複製」と呼ばれます)が実行されます。このトピックのより広範な紹介については、嘘をついているを参照してください。

PHP 5

内部配列ポインターとHashPointer

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

一方でforeach唯一のIAPがありますが、1つのアレイは、複数の一部にすることができます: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) {
        // ...
    }
}

内部配列ポインターが1つだけの2つの同時ループをサポートするにforeachは、次の処理を実行します。ループ本体が実行される前foreachに、現在の要素へのポインターとそのハッシュをfor-eachごとにバックアップします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);

ここで$arrは、IAPの変更がに$arrリークするのを防ぐために複製され$outerArrます。上記の条件に関して、配列は参照ではなく(is_ref = 0)、2つの場所で使用されます(refcount = 2)。この要件は残念であり、次善の実装の成果物です(ここでは反復中に変更の心配はないため、最初にIAPを使用する必要はありません)。

(*)refcounthereのインクリメントは無害に聞こえますが、copy-on-write(COW)セマンティクスに違反しています:これは、refcount = 2配列のIAPを変更しようとしているのに対し、COWは変更がrefcount =でのみ実行できることを示しています1つの値。反復配列のIAP変更は観測可能であるため、この違反はユーザーに見える動作の変化(COWは通常は透過的です)をもたらしますが、配列の最初の非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();
}

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

例:テストケース

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

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

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

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

  • 4テストケースおよび5の両方eachresetすることにより、基準関数です。$array持っているrefcount=2、それが重複する必要があるので、それは、彼らに渡されたとき。そのためforeach、別のアレイで再び動作します。

例:currentforeachの影響

さまざまな複製動作を示す良い方法はcurrent()foreachループ内の関数の動作を観察すること です。この例を考えてみましょう:

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

ここでcurrent()は、配列を変更しなくても、それが参照関数(実際には優先参照)であることを知っておく必要があります。これは、他のすべての関数next(参考文献など)とうまく機能するために必要です。参照による通過アレイを分離する必要があり、従ってことを意味する$arrayforeach-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()関数に渡すときに配列を複製する必要がなくなりました。このようにcurrent()してforeach、同じアレイ上の作品。ただし、foreachポインタを進める方法が原因で、1つずれた動作が表示されます。

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

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

ここでは$array、ループの開始時のrefcount は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'、アロケーターはたまたま同じメモリー位置を再利用してelementを格納します。そのため、foreachは、新しく挿入された要素に直接ジャンプすることになり、ループが短縮されます。

ループ中に反復エンティティを置換する

私が言及したい最後の奇妙なケースの1つは、ループ中に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()その上での相互作用などをサポートするために1つの配列ポインターを拡張する必要があったため、これはやや最適ではありませんでした。

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

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

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

2番目の例のセットは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の復元メカニズムは、削除された要素と同じように「見えた」ため(ハッシュとポインタの衝突により)、新しい要素に直接ジャンプしました。要素のハッシュに依存しなくなったため、これは問題ではなくなりました。


4
@馬場そうです。関数に渡すの$foo = $arrayはループの前と同じです;)
NikiC

32
zvalが何であるかわからない場合は、サラゴールマンのブログ
shu zOMG chen

1
マイナーな修正:バケットと呼ぶものは、通常ハッシュテーブルでバケットと呼ばれるものではありません。通常、バケットは同じハッシュ%サイズのエントリのセットです。通常はエントリと呼ばれるものに使用しているようです。リンクリストはバケットではなく、エントリにあります。
unbeli

12
@unbeli PHPで内部的に使用されている用語を使用しています。BucketSは、注文のハッシュ衝突し、また二重連結リストの一部のための二重リンクリストの一部である;)
NikiC

4
素晴らしいアンサー。あなたが意味したと思うiterate($outerArr);iterate($arr);どこかではなく、と。
niahoo 2016年

116

例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項演算子:高速かどうか。


あなたの権利のようです、私はそれを示すいくつかの例を作りました: codepad.org/OCjtvu8rあなたの例との違いの1つ-値を変更してもコピーされず、キーを変更した場合のみです。
zb

これは確かに上記のすべての動作を説明しておりeach()、最初のテストケースの最後に呼び出すと、元の配列の配列ポインターが2番目の要素を指していることがわかります。最初の反復。これforeachは、ループのコードブロックを実行する前に配列ポインターを移動することも示しているようですが、これは予期していませんでした。多くの感謝、これは私のためにそれをうまくクリアします。
DaveRandom

49

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

A)foreach上で動作見込みコピー元の配列の。これはforeach()foreach Notes / Userコメントがprospected copy作成されない限り、または作成されない限り、SHAREDデータストレージを保持することを意味します

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

c)元の配列とforeach()イテレータにはDISTINCT SENTINEL VARIABLES、があります。つまり、1つは元の配列用、もう1つは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

2
あなたの答えは正確ではありません。foreachアレイの潜在的なコピーを操作しますが、必要でない限り実際のコピーは作成しません。
linepogl 2012

その潜在的なコピーがいつどのようにコードを通じて作成されるかを示したいですか?私のコードforeachは、配列を100%コピーしていることを示しています。私は知りたいと思っています。コメントをありがとう
sakhunzai

アレイのコピーには多くのコストがかかります。forまたはのいずれかを使用して、100000要素の配列を反復処理するのにかかる時間を数えてみてくださいforeach。実際のコピーは行われないため、これら2つの間に大きな違いはありません。
linepogl 2012

次に、SHARED data storageまで、またはなしが予約されていると想定しますcopy-on-writeが、(私のコードスニペットから)にSENTINEL variablesは、original arrayとの2 つのセットが常に2 つあることは明らかですforeach。意味のある感謝
sakhunzai

1
はい、それは「予想される」コピー、つまり「潜在的な」コピーです。あなたが提案したように保護されていません
sakhunzai

33

PHP 7に関する注意

人気が高まったため、この回答を更新するには:この回答はPHP 7以降では適用されません。「下位互換性のない変更」で説明されているように、PHP 7ではforeachは配列のコピーで機能するため、配列自体に変更がありますforeachループには反映されません。詳細はリンク先をご覧ください。

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

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

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

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」は実際にどのように機能するのですか?これは、この動作の背後にある内部を説明しています。


7
さてあなたは答えの残りを読みましたか?foreachは、コードを実行するに別の時間ループするかどうかを決定することは完全に理にかなっています。
dkasipovic 14

2
いいえ、配列は変更されますが、foreachはすでにそれが最後の要素(反復の開始時)にあると「考えて」おり、ループしないため、「遅すぎます」。2番目の例では、反復の開始時に最後の要素ではなく、次の反復の開始時に再び評価されます。テストケースを用意しようとしています。
dkasipovic 2014

1
で@AlmaDo見lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509それが反復するときには、常に次のポインタに設定されています。そのため、最後の反復に到達すると、(NULLポインターを介して)完了とマークされます。その後、最後の反復でキーを追加しても、foreachはそれを認識しません。
bwoebi 2014

1
@DKasipovicいいえ。そこには完全で明確な説明はありません(少なくとも今のところ、私は間違っている可能性があります)
Alma Do

4
実際、@ AlmaDoは彼自身の論理を理解するのに欠陥があるようです…あなたの答えは結構です。
bwoebi 2014

15

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

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


13

多くの開発者は、経験豊富な開発者でさえも、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が作成された理由がわかります。コピーが作成され、ループ構造自体の構造によってのみ使用される場合、配列はループの実行中は静的なままなので、問題が発生することはありません。


7

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


1
Object public variables間違っているか、せいぜい誤解を招く。正しいインターフェイス(Traversibleなど)がないと、配列内のオブジェクトを使用できませforeach((array)$obj ...ん。実際には、オブジェクトではなく単純な配列で作業しています。
クリスチャン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.