5771


この質問は `foreach` の内部的な挙動についてのものです。

PHP の `foreach` は、イテレーション対象の配列をコピーして動作しているのだと思っています。

### ケース1:

[code]

    $array = array(1, 2, 3);
    foreach ($array as $item) {
        echo $item, ' ';
        $array[] = $item;
    }
    echo PHP_EOL, json_encode($array), PHP_EOL;
    
    /* output:
        1 2 3
        [1,2,3,1,2,3]
     */
    
[/code]

もし、イテレーション対象の配列そのものとして動作するなら、上記スクリプトは（ループ中に配列に対して要素を追加しているため）無限ループとなるはずだからです。

`foreach` についての [PHP マニュアル](http://php.net/manual/ja/control-
structures.foreach.php) を見たところ、以下の記述がありました。

> foreach の実行開始時に内部配列ポインタは、 配列の先頭要素を指すように自動的にリセットされます。

そこでまず、以下のケース2を試してポインタの位置を確かめました。

### ケース2:

[code]

    $array = array(1, 2, 3);
    // 配列のポインタを次に進めます。（が、foreach によりリセットされるため影響しないはずです）
    each($array);
    
    foreach ($array as $item) {
        echo $item, " ";
    }
    
    // foreach によりポインタが最後に進んでるはずなので、false が返って来るはず
    var_dump(each($array));
    
    /* output:
        1 2 3
        bool(false)
    */
    
[/code]

予想通りの結果が得られました。

[PHP マニュアル](http://php.net/manual/ja/control-structures.foreach.php)
には以下の記述もあります。

> foreach は内部の配列ポインタに依存するので、 ループ内で配列ポインタを変更すると予期せぬ振る舞いを引き起こします。

「予期せぬ振る舞い」が何なのか試すために、以下のケース3を実行してみます。

### ケース3

[code]

    $array = array(1, 2, 3);
    foreach ($array as $key => $item) {
        echo $item, " ";
        reset($array); // 配列ポインタを強制リセット
    }
    
    /* output:
        1 2 3
    */
    
[/code]

結果は、「予期せぬ振る舞い」ではなく、配列のコピーに対して `foreach` が動作しているように見えるだけでした。

## 質問

`foreach` はその配列のコピーで動作しているように見えますが、ループが終了した後に元配列の配列ポインタを末尾に移動しているようです。

`foreach` は本当のところどのように動作しているのでしょうか？

また、どのような場合に「予期せぬ振る舞い」が出るのでしょうか？

> この質問は以下の本家StackOverflowの質問を翻訳・編集したものです。  
>  [How 'foreach' actually
> works](https://stackoverflow.com/q/10057671/4369063)
> [@DaveRandom](https://stackoverflow.com/users/889949/daverandom)


CC BY-SA 3.0
2015-02-02T08:32:05.623
5770
2015-02-09T02:33:49.250
2017-05-23T12:38:56.467
-1
2944
question
php
PHP 'foreach' の内部挙動について


注：この答えは PHP の`zval` コンテナについての知識があることを前提としています。特に `is_ref` や `refcount`
が何者かを知っている必要があります。  
それらについては、[PHP マニュアル](http://php.net/manual/ja/features.gc.refcounting-
basics.php)も参照してください。

`foreach` は配列やプレーンなオブジェクト、`Traversable` オブジェクトで行えますが、ここでは特別に記述がない場合は配列を
`foreach` する際についての説明をします。

まず、配列の `foreach` の挙動について理解する上で重要となる、配列とそのイテレーションについての内部挙動を説明します。

# 配列イテレーションの内部挙動

PHP の配列は `HashTable` により管理されており、また、`foreach` はそれに基づいて配列を走査します。

PHP は内部的に配列を行き来するための2つのメカニズムを持っています。

1つ目は、内部配列ポインタです。このポインタは、`HashTable` 構造体の一部であり、基本的に現在の `HashTable` の `Bucket`
へのポインタです。内部配列ポインタは変更に対して安全で、現在の `Bucket` が削除されると、内部配列ポインターは次の `Bucket`
を指すように更新されます。

2つ目は、`HashPosition` と呼ばれる、外部の配列ポインタです。これは基本的に内部配列ポインタと同じですが、`HashTable`
内に保持されず、この外部の配列ポインタは変更に対して **安全ではありません** 。（例えば `HashTable` が指している `Bucket`
が削除されてもそのまま残り、セグメンテーションフォルトを起こす可能性があります。）

そのため、PHP の内部多くのケースでは内部配列ポインタが使用されます。

内部配列ポインタの問題は、それが `HashTable` の一部だということです。それを変更するとき、その `HashTable` も変更されてしまいます。  
そのため、通常は配列をイテレーションさせる際に配列をコピーします。

例えばネストされたイテレーションを見れば、なぜコピーが必要なのか分かると思います。

[code]

    $array = array(1, 2, 3);
    foreach ($array as $key => $item) {
        echo 'outer foreach: ', $key, ' => ', $item, PHP_EOL;
        foreach ($array as $key2 => $item2) {
            echo '    inner foreach: ', $key2, ' => ', $item2, PHP_EOL;
        }
    }
    
    /* output:
        outer foreach: 0 => 1
            inner foreach: 0 => 1
            inner foreach: 1 => 2
            inner foreach: 2 => 3
        outer foreach: 1 => 2
            inner foreach: 0 => 1
            inner foreach: 1 => 2
            inner foreach: 2 => 3
        outer foreach: 2 => 3
            inner foreach: 0 => 1
            inner foreach: 1 => 2
            inner foreach: 2 => 3
    */
    
[/code]

ここでは同じ配列を別々に、配列ポインタを共有せずにループします。

# foreach での配列イテレーション

## コピーの動作

`foreach` が配列をイテレーションする前に、なぜ配列をコピーする必要があるのかわかったと思います。  
ですが、これが全てではありません。イテレーション対象の配列がコピーされるかどうかは以下の様な条件によります。

  * イテレーションされる配列がリファレンスである場合は、コピーは行わず、代わりに `addref` が行われます。
[code]     $array = array(1, 2, 3);

    $ref =& $array; // $array はこの時点で is_ref=1
    foreach ($array as $val) {
        // ...
    }
    
[/code]

この場合、配列の変化は内部の配列ポインタを含めてリファレンスに伝播する必要があるため、 `foreach`
が配列をコピーするとリファレンスの動作が壊れてしまいます。

  * 配列の `refcount` が `1` の場合コピーは行われません。  
`refcount=1`はその配列が他の場所で使用されていないことを意味し、`foreach` はそれを直接使用できます。  
`refcount` が `1` よりも大きい場合、その配列が他の変数と共有されていることを意味し、上記のリファレンスのケースとは異なり、変更を避けるために
`foreach` は配列をコピーします。

  * 配列がリファレンスによってイテレートされる（`foreach ($array as &$ref)`）場合、リファレンスを参照します。

## イテレーションの動作

実際のイテレーションはどのように行われるのでしょう。  
「通常」のイテレーションのパターン（foreach 以外でPHPで普通用いられるもの）は以下の様なものです（擬似コードで示します）。

[code]

    reset();
    while (get_current_data(&data) == SUCCESS) {
        code();
        move_forward();
    }
    
[/code]

それに対して `foreach` のイテレーションは以下の様な実装になっています。

[code]

    reset();
    while (get_current_data(&data) == SUCCESS) {
        move_forward();
        code();
    }
    
[/code]

2つは `move_forward()` の位置が違い、`foreach` の場合、ループ中のユーザーコードの後ではなく、前に
`move_forward()` が呼び出されます。つまり、ユーザー側のコードが要素 `$i` に対して処理しているとき、内部配列ポインタはすでに
`$i+1` を指しているのです。

`foreach` 中に現在の `Bucket` が削除されても、 **次の** `Bucket` を指している理由はこのためです。

## コードの意味

`foreach`
は配列をイテレートする多くのケースでコピーを行います。これはコピーを行わない場合と比べて遅くなりますが、わざとらしいベンチマークのようなもので無い限り、パフォーマンスへの影響は気にするほどではないです。

通常の使い方をしている限り `foreach`
の挙動はユーザに分かりやすく、どのようにコピーが行われて、今どこにポインタがあるかなどを意識する必要はありません。

  * ケース1：ループ前の `$array` は `refcount=1` です。配列はコピーされず、`addref` が行われます。また、`$array[]` により `zval` が分離されるので、要素を追加している配列とイテレーションで見ている配列は別物となります。

  * ケース2：これも同様です。`foreach` ループの前は `refcount=1` で、`addref` のみが行われ、`$array` の内部ポインタが変更されます。それにより、ループの最後にポインターは `NULL`（イテレーション終了を表す）となります。`each` はそれを示す `false` が返ります 。

  * ケース3：`reset` はリファレンスによる関数です。`$array`が `refcount=2` となるとめ、配列は分離されます。だからここでは分離された別々の配列として動作します。

しかし、これらのテストケースでは説得力がありません。この挙動は、ループで `current` のような関数を使用する場合、本当にわかりにくいものとなります。

[code]

    $array = array(1, 2, 3);
    foreach ($array as $val) {
        var_dump(current($array));
    }
    /* Output: 2 2 2 2 2 */
    
[/code]

`current` はリファレンスを操作する関数で、リファレンスを使って現在の値を返します。`current` により `zval`
が分離され、それ以降、`$array` と `foreach` の配列は別物となります。なぜ、2が返って来るかは上に書いたとおりで、`foreach`
がユーザーコードの後ではなく、それより前に、配列ポインタを進めているからです。なので、ループの最初の要素の時でも、`foreach`
によって2番目の要素にポインタがあります。

少し変更して試します。

[code]

    $array = array(1, 2, 3);
    $ref = &$array;
    foreach ($array as $val) {
        var_dump(current($array));
    }
    /* Output: 2 3 false */
    
[/code]

これは `is_ref=1` のケースです。この場合、配列は（上の例のように）コピーされません。  
`is_ref` である配列はリファレンスを操作する `current` 関数に `$array` を渡すときに配列を分離しません。なのでこの例では
`current` で操作している配列と `foreach` の配列は全く同じものとなります。ここでも、`foreach`
がポインターを先に進めるため1つずれて動作しているのが分かります。

以下の場合でも同様にリファレンスによるイテレーションとなります。

[code]

    $array = array(1, 2, 3);
    foreach ($array as &$val) {
        var_dump(current($array));
    }
    /* Output: 2 3 false */
    
[/code]

ここで重要なのはリファレンスでイテレートする場合、`foreach` が `$array` を `is_ref=1`
にするということです。そのため、基本的には上の例と同じ状況になります。

配列を別の変数に代入する例を試します。

[code]

    $array = array(1, 2, 3);
    $foo = $array;
    foreach ($array as $val) {
        var_dump(current($array));
    }
    /* Output: 1 1 1 */
    
[/code]

ここではループが開始されたときの `$array` の `refcount` は2です。これにより、ループ処理の前に配列がコピーされ、`$array` と
`foreach`
で使用される配列は、開始から完全に別物です。よって、ループ直前の内部配列ポインターの位置が取得されます。上記では（ループ前に）内部配列ポインタは、最初の要素を指していました。  
もちろん以下のようにポインタが最後にあった場合、その要素が出力されます。

[code]

    $array = array(1, 2, 3);
    end($array); // ポインタを最後にセット
    $foo = $array;
    foreach ($array as $val) {
        var_dump(current($array));
    }
    /* Output: 3 3 3 */
    
[/code]

# オブジェクトのイテレーション

オブジェクトのイテレーションには2つのケースがあります。

a) `Traversable` でない（または、内部に `get_iterator`
ハンドラがない）オブジェクトの場合、イテレーションは配列と非常に似た動作となり、同様のコピー動作が適用されます。唯一の違いは `foreach`
が現在のスコープから見えないプロパティをスキップするためのコードを追加で実行します。

  * 宣言されたプロパティの場合、PHP は、プロパティのハッシュテーブルを先に最適化します。
  * プロパティのハッシュテーブルはイテレーション毎に再フェッチされます。つまり、PHPが `get_properties` ハンドラを何度も呼び出します。「通常の」プロパティでは少し違いますが、プロパティをハンドラで動的に生成する場合、それはプロパティテーブルが毎回再計算されます。

b) `Traversable` なオブジェクト 。この場合、ほとんどすべての上記のことが **当てはまりません**
。PHPは、コピーをせず、「ループの前にポインタを進める」トリックを使うこともありません。`Traversables`
でのイテレーション動作は予測可能で、特に説明することはありません。

# ループ中でのイテレートされたエンティティの置き換え

まだ説明していないもう一つの特殊なケースはを説明します。

PHP
ではループ中にイテレートされたエンティティを置き換えできます。配列のイテレーションを開始した後、途中で別の配列に置き換えたり、配列のイテレーションを開始した後、それをオブジェクトにすり替えたりできます。

[code]

    $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 */
    
[/code]

このケースのように、置き換えが起こると、PHP は最初からイテレーションを行います。

# イテレーション中の内部配列ポインタの変更

最後にイテレーション中に内部配列ポインタを変更した場合、何が起きるのか説明します。

リファレンスによるループ中に `next` や `prev`
を呼び出すと、内部配列ポインタが移動されることがわかりますが、それはイテレーション動作に影響はありません。なぜなら `foreach`
は現在の位置と現在の要素のハッシュを `HashPointer` にバックアップしているからです。`foreach`
が次のイテレーションに進むとき、内部位置の変更をチェックし、（バックアップされたハッシュに基づき）それを復元しようとします。

実際に「試して」みましょう。  
まずは、内部ポインタの変更が `foreach` の動作に影響しないケースです。

[code]

    $array = [1, 2, 3, 4, 5];
    $ref =& $array;
    foreach ($array as $value) {
        var_dump($value);
        reset($array);
    }
    // output: 1, 2, 3, 4, 5
    
[/code]

次に、要素のアンセットを試します。

[code]

    $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
    
[/code]

2回表示された1により、このとき `reset` が起こっています。これは、バックアップハッシュのある要素が削除されたためです。

次にハッシュの衝突を試します。

[code]

    $array = ['test' => 1, 'EzFY' => 2, 'FYEz' => 3, 'FYFY' => 4];
    $ref =& $array;
    foreach ($array as $value) {
        unset($array['EzFY']);
        $array['FYFZ'] = 5;
        reset($array);
        var_dump($value);
    }
    // output: 1 1 3 4 5
    
[/code]

これは予想通り動作します。（最初のループで `foreach` が現在指していた）`EzFY` キーを削除した結果、`reset`
が起きました。そして、配列に追加した要素（`FYFZ` キー）がイテレーションの最後（`5`）に追加されました。

もし、`FYFZ` ではなく `EzEz` キーだとどうなるのか見てみたいと思います。

[code]

    $array = ['test' => 1, 'EzFY' => 2, 'FYEz' => 3, 'FYFY' => 4];
    $ref =& $array;
    foreach ($array as $value) {
        unset($array['EzFY']);
        $array['EzEz'] = 5;
        reset($array);
        var_dump($value);
    }
    
    // output 1 5
    
[/code]

そうするとループが他のエレメントをスキップし、新たな要素に直接飛びました。  
それは、`EzEz` キーが `EzFY` キーと衝突しているからです。（`EzEz`, `EzFY`, `FYEz`, `FYFY`
は同じハッシュ値になるため衝突しています。）さらに、削除された `EzFy` のバケットは `EzEz` と同じメモリアドレスにあり、PHP はそこにまだ
`Bucket` があると見なします。その結果、`foreach` が配列ポインタを「復元」し、配列の最後の要素にジャンプします。

この回答のコードは PHP 5.6.4 で動作させ確認を行っています。  
なお、PHP 7 以降については、zval
の構造などが変わりますので、一部の特殊な挙動は同様に動作しなくなる可能性があります。（実際に「イテレーション中の内部配列ポインタの変更」などでの一部の挙動が変わっているのを確認しています。）

> この回答は、本家StackOverflowの
> @[NikiC](https://stackoverflow.com/users/385378/nikic) さんの下記回答を翻訳・編集したものです。  
>  <https://stackoverflow.com/a/14854568/4369063>


CC BY-SA 3.0
2015-02-02T08:32:05.623
5771
2015-02-05T02:14:26.783
2017-05-23T12:38:55.250
-1
2944
5770
answer