10

foreach() ループ中の配列の変更に関するクエリがいくつかあります。以下のコードでは、クロージャ/コールバックを含む 3 つの配列をループし、それぞれを呼び出します。反復中に各配列の最後にクロージャーを追加しますが、配列のサイズが変更されたことを foreach() が認識しない場合があるため、追加されたクロージャーが呼び出されません。

class Foo
{
    private $a1 = array();
    private $a2 = array();

    public function f()
    {
        echo '<pre style="font-size: 20px;">';
        echo 'PHP: ' . phpversion() . '<br><br>';

        $this->a1[] = function() { echo 'a1 '; };
        $this->a1[] = array($this, 'g');
        foreach ($this->a1 as &$v)
        {
            // The callback added in g() never gets called.
            call_user_func($v);
            //echo 'count(v) = ' . count($v) . ' ';
        }

        echo '<br>';

        // The same thing works fine with a for() loop.
        $this->a2[] = function() { echo 'a2 '; };
        $this->a2[] = array($this, 'h');
        for ($i = 0; $i < count($this->a2); ++$i)
            call_user_func($this->a2[$i]);

        echo '<br>';

        // It also works fine using a local array as long as it
        // starts off with more than one element.
        $a3[] = function() { echo 'a3 '; };
        //$a3[] = function() { echo 'a3 '; };
        $i = 0;
        foreach ($a3 as &$x)
        {
            call_user_func($x);
            if ($i++ > 1) // prevent infinite loop
                break;

            // Why does this get called only if $a3 originally starts
            // with more than one element?
            $a3[] = function() { echo 'callback '; };
        }

        echo '</pre>';
    }

    private function g()
    {
        echo 'g() ';
        $this->a1[] = function() { echo 'callback '; };
    }

    private function h()
    {
        echo 'h() ';
        $this->a2[] = function() { echo 'callback '; };
    }
}

$foo = new Foo;
$foo->f();

出力:

PHP: 5.3.14-1~dotdeb.0

a1 g() 
a2 h() callback 
a3

期待される出力:

a1 g() callback
a2 h() callback 
a3 callback

$a3ループの前に 2 番目の要素のコメントを外した場合の出力:

a3 a3 callback
  1. 最初のループが反復する別の要素をforeach ($this->a1 as &$v)認識しないのはなぜですか?$v
  2. 配列が複数の要素で始まる場合にのみ$a3、3 番目のループ中に変更が機能するのはなぜですか?foreach ($a3 as &$x)

反復中に配列を変更することはおそらく良い考えではないことを認識していますが、PHP はそれを許可しているように見えるので、なぜ上記のように機能するのか興味があります。

4

2 に答える 2

4

興味深い観察:

echo "foreach:  ";
$a = array(1,2,3);
foreach($a as $v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nforeach&: ";
$a = array(1,2,3);
foreach($a as &$v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nwhile:    ";
$a = array(1,2,3);
while(list(,$v) = each($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nfor:      ";
$a = array(1,2,3);
for($v=reset($a); key($a)!==null; $v=next($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

結果は

foreach:  1 2 3 
foreach&: 1 2 3 4 
while:    1 2 3 4 5 
for:      1 2 3 4 5 

これの意味は:

  • 通常のforeachループは配列のコピーで動作し、ループ内の配列の変更はループに影響しません
  • 参照された値を持つaは、元の配列を使用することを強制されますが、キーと値の変数を割り当てた後、各反復のforeachに配列ポインターを進めます。また、ポインターが最後に到達するとすぐに別のチェックを防止する最適化が行われています。したがって、最後の反復の開始時に、ループはもう一度実行されてから終了するように指示されます。これ以上干渉することはありません。
  • 配列ポインタを進めるwhileループですが、最後の反復後に再度明示的にチェックしますeach()foreach
  • 各反復の後forに配列ポインターが進められるループでは、任意の時点で配列を変更しても明らかに問題はありません。
于 2013-01-31T23:54:59.627 に答える
3

1.最初のループforeach($ this-> a1 as&$ v)が、$ vに反復する別の要素があることに気付かないのはなぜですか?

この動作は、各反復で配列上で内部ポインターが進められているためと思われます。配列の最後の反復で配列の最後に配列要素を追加する、つまり内部ポインターがすでにnullの場合は、この要素が繰り返されないことを意味します。コードにいくつかの変更を加えると、これを確認できます。

class Foo
{
    private $a1 = array();
    private $a2 = array();

    public function f()
    {
        echo '<pre style="font-size: 20px;">';
        echo 'PHP: ' . phpversion() . '<br><br>';

        $this->a1[] = function() { echo 'a1 <br/>'; };
        $this->a1[] = array($this, 'g');
        foreach ($this->a1 as $key => &$v)
        {
           //lets get the key that the internal pointer is pointing to 
           // before the call.
                  $intPtr = (key($this->a1) === null) ? 'null' : key($this->a1);
                echo 'array ptr before key ', $key, ' func call is ',    
                       $intPtr, '<br/>' ;
            call_user_func($v);
            //echo 'count(v) = ' . count($v) . ' ';
        }

        echo '<br><br>';

        // The same thing works fine with a for() loop.
        $this->a2[] = function() { echo 'a2 '; };
        $this->a2[] = array($this, 'h');
        for ($i = 0; $i < count($this->a2); ++$i)
            call_user_func($this->a2[$i]);

        echo '<br><br>';

        // It also works fine using a local array as long as it
        // starts off with more than one element.
        $a3[] = function() { echo 'a3 '; };
        //$a3[] = function() { echo 'a3 '; };
        $i = 0;
        foreach ($a3 as &$x)
        {
            call_user_func($x);
            if ($i++ > 1) // prevent infinite loop
                break;

            // Why does this get called only if $a3 originally starts
            // with more than one element?
            $a3[] = function() { echo 'callback '; };
        }

        echo '</pre>';
    }

    private function g()
    {
        echo 'g() <br>';
        $this->a1[] = function() { echo 'callback '; };
    }

    private function h()
    {
        echo 'h() <br>';
        $this->a2[] = function() { echo 'callback '; };
    }
}

$foo = new Foo;
$foo->f(); 

出力:

array ptr before key 0 func call is 1
a1 
array ptr before key 1 func call is null <-will not iterate over any added elements!
g() 

a2 h() 
callback 

a3

2. $ a3の変更が3番目のループforeach($ a3 as&$ x)で機能するのはなぜですか?ただし、配列が複数の要素で始まる場合に限りますか?

もちろん、内部ポインタがnullを返す前に配列に要素を追加すると、その要素は繰り返されます。あなたの場合、配列に1つの要素がある場合、最初の反復で内部ポインターはすでにnullを返しています。ただし、最初に複数の要素がある場合は、最初の反復で追加の要素を追加できます。これは、この時点で内部ポインターが2番目の初期要素を指しているためです。

于 2013-01-31T22:11:18.253 に答える