web-dev-qa-db-ja.com

どうやって PHP 「foreach」は実際に動作しますか?

私は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が最初に実行を開始すると、内部配列ポインタは自動的に配列の最初の要素にリセットされます。

そうです…これは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は配列のcopyで動作しますが、ループの後でソース配列の配列ポインタを配列の最後に設定します。

  • これは正しいのでしょうか。
  • そうでなければ、それは本当に何をしているのでしょうか?
  • foreach中に配列ポインタを調整する関数(each()reset()など)を使用すると、ループの結果に影響を与える可能性がある状況はありますか?
1824
DaveRandom

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

これが、このコピーオンライトの振る舞いによるもう1つの副作用についての優れた記事です。 PHP三項演算子:高速かどうか

106
linepogl

foreach()を扱う際に注意すべきいくつかの点があります。

a)foreachは、元の配列の予想されるコピーに作用します。これは、prospected copyが作成されないかぎり、foreach()がSHAREDデータストレージを持つことを意味します 各Notes/Userのコメントに対して .

b)何が見込みのあるコピーを引き起こすのか?見込みのあるコピーはcopy-on-writeのポリシーに基づいて作成されます。つまり、foreach()に渡される配列が変更されるたびに、元の配列のクローンが作成されます。

c)元の配列とforeach()イテレータはDISTINCT SENTINEL VARIABLESを持ちます。つまり、元の配列用とforeach用です。下記のテストコードを参照してください。 SPLイテレーター配列イテレーター

Stack Overflow question PHPのforeach 'ループで値が確実にリセットされるようにするにはどうすればよいですか? _あなたの質問のケース(3,4,5)に対処します。

次の例は、each()とreset()がforeach()イテレータのSENTINEL変数(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
42
sakhunzai

PHP 7のメモ

この回答を人気のあるものに更新するには:この回答はPHP 7以降では適用されません。「 後方互換性のない変更 」で説明されているように、PHPの7配列のコピーなので、配列自体の変更はforeachループには反映されません。リンクでより多くの詳細。

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

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

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

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

これはすべての繰り返しの結果であると私は考えています それぞれの繰り返しで おそらくforeachname__が{}のコードを呼び出す前にすべてのロジックを実行するということです。

テストケース

あなたがこれを実行するならば:

<?
    $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
)

これは配列が変更されたことを意味しますが、foreachname__がすでに配列の最後の要素にあるときに変更したため、ループしないことを「決定」し、新しい要素を追加しても「遅すぎ」て追加それはループされませんでした。

詳細な説明は PHP 'foreach'で実際にどのように機能しますか?で読むことができますか? は、この振る舞いの背後にある内部構造を説明しています。

29
Damir Kasipovic

PHP manualで提供されているドキュメントのとおり。

繰り返しごとに、現在の要素の値が$ 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);
}

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

14
user3535130

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

11
hrvojeA

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 indexesreferencesとして使用することはできません。

出所: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

7
Pranav Rana