嵌套的foreach与迭代器接口

时间:2010-08-04 12:31:14

标签: php iterator foreach

<? foreach ($this->criteria as $key => $value): ?>
<li><?= $this->accommodationsLink($this->criteria, $key) ?></li>
<? endforeach ?>

此代码会产生意外结果,因为只能看到一个链接。但是$ this-&gt;条件中有两项。

我探究了问题的原因。在函数accommodationLink中,另一个foreach循环适用于相同的标准对象

foreach ($criteria as $key => $value) {
    $params[$key] = $value;
}

$ this-&gt; criteria和$ criteria是实现php Iterator接口的同一对象。是否有一种简单的方法可以让这个代码工作或嵌套的foreach循环不能用php迭代器接口?

3 个答案:

答案 0 :(得分:2)

嗯,第二个foreach将在跑步前打电话给$iterator->reset()。所以当第二个foreach到达迭代器的末尾时,内部指针已经在数组的末尾......

就像是:

$it->reset();
while ($it->valid()) {
   $it->reset();
   while ($it->valid()) {
       //do something
       $it->next();
   }
   $it->next();
}

在外部循环中购买它到$it->next()调用的时间,它已经无效。因此next()调用将“失败”,$it->valid()将返回false。

迭代器不是问题,这是你正在使用的逻辑的一个问题。如果你真的必须嵌套循环,那么clone内部循环中的迭代器($subit = clone $it),这样你就不会打扰指针......

修改:克隆示例:

$it->reset();
while ($it->valid()) {
   $bar = clone $it;
   $bar->reset();
   while ($bar->valid()) {
       //do something
       $bar->next();
   }
   $it->next();
}

或者,使用foreach(在语义上等效):

foreach ($it as $key => $value) {
    $subit = clone $it;
    foreach ($subit as $k => $v) {
        //Do stuff
    }
}

答案 1 :(得分:1)

我尝试使用普通数组和PHP迭代器。不幸的是,PHP迭代器,因为它们是对象,所以工作方式不同。对象通过引用传递,而数组是按值。因此,当嵌套的foreach到达迭代器的末尾时,第一个foreach无法恢复停止的位置,因为内部指针设置为最后一个元素。

考虑使用普通PHP数组编写的以下示例:

$test = [1, 2, 3];

foreach ($test as $i1 => $v1) {
    echo "first loop: $i1\n";

    foreach ($test as $i2 => $v2) {
        echo "second loop: $i2\n";
    }
}

以上代码段产生以下输出:

first loop: 0
second loop: 0
second loop: 1
second loop: 2
first loop: 1
second loop: 0
second loop: 1
second loop: 2
first loop: 2
second loop: 0
second loop: 1
second loop: 2

如果我们使用迭代器尝试相同的事情,我们会得到完全不同的结果。为了避免混淆,我将使用ArrayIterator类,以便所有内容都已由PHP人员实现,并且我们最终不会以错误的方式使用接口。所以这里没有错误的余地,这就是迭代器的实现方式:

$test = new ArrayIterator([1, 2, 3]);

foreach ($test as $i1 => $v1) {
    echo "first loop: $i1\n";

    foreach ($test as $i2 => $v2) {
        echo "second loop: $i2\n";
    }
}

输出是:

first loop: 0
second loop: 0
second loop: 1
second loop: 2

正如您所看到的,第一个foreach只执行一次。

解决方法可能是实现SeekableIterator接口。它允许我们使用 seek()方法将内部指针重置为正确的值。在我看来这是一个不好的做法,但如果PHP家伙不解决这个问题我真的不能说它最好。从现在开始我可能会避免使用迭代器,因为它们似乎与数组的行为不同,我认为这是人们最初的假设。所以使用它们会使我的应用程序容易出错,因为我的团队中的开发人员可能不知道这一点并且对代码感到困惑。

使用 SeekableIterator 界面的示例:

class MyIterator implements SeekableIterator
{
    private $position = 0;
    private $array = [1, 2, 3];

    public function __construct()
    {
        $this->position = 0;
    }

    public function rewind()
    {
        $this->position = 0;
    }

    public function current()
    {
        return $this->array[$this->position];
    }

    public function key()
    {
        return $this->position;
    }

    public function next()
    {
        ++$this->position;
    }

    public function valid()
    {
        return isset($this->array[$this->position]);
    }

    public function seek($position)
    {
        $this->position = $position;
    }
}

$test = new MyIterator();

foreach ($test as $i1 => $v1) {
    echo "first loop $i1\n";

    foreach ($test as $i2 => $v2) {
        echo "second loop $i2\n";
    }

    $test->seek($i1);
}

输出与任何人预期的一样:

first loop: 0
second loop: 0
second loop: 1
second loop: 2
first loop: 1
second loop: 0
second loop: 1
second loop: 2
first loop: 2
second loop: 0
second loop: 1
second loop: 2

这一切都发生了,因为每个foreach都在自己的数组副本上工作。迭代器,因为它们是对象,通过引用传递。因此每个foreach共享相同的对象。如果您尝试在嵌套的foreach中取消设置元素,则会发生同样的情况。 unset将增加内部指针。然后执行到达嵌套foreach的末尾,内部指针再次增加。这意味着在未设置的情况下,我们将内部指针增加两次。因此,父foreach将跳过一个元素。

我的建议是,如果你无法避免迭代器,那真的非常小心。始终对它们进行彻底的单元测试。

注意:在PHP 5.6.14和PHP 7.0.0 RC5上进行了代码测试。

答案 2 :(得分:1)

修改: 在发布之后,我意识到如果你在嵌套的foreach中continuebreak,这将会破坏。所以这可能不是您理想的解决方案。

正如其他答案中所述,PHP foreach在rewind循环的开头调用foreach,并在每次迭代结束时调用valid。因此,嵌套的foreach迭代器变得无效并在父foreach中保持这种状态。这是一个使用堆栈指针而不是单个指针的hackish变通方法,并使这个迭代器在这种情况下表现得像数组。

class Test implements Iterator {
    private $loopstack = [];

    private $array = array("A", "B", "C",);

    function rewind() {
        $this->loopstack[] = 0;
    }

    function current() {
        return $this->array[end($this->loopstack)];
    }

    function key() {
        return end($this->loopstack);
    }

    function next() {
        array_push($this->loopstack, array_pop($this->loopstack) + 1);
    }

    function valid() {
        $valid = isset($this->array[end($this->loopstack)]);
        if (!$valid) {
            array_pop($this->loopstack);
        }
        return $valid;
    }
}

$iterator = new Test();
foreach ($iterator as $e){
    var_dump('loop1 ' . $e);
    foreach ($iterator as $e2){
        var_dump('loop2 ' . $e2);
    }
}

输出:

string(7) "loop1 A"
string(7) "loop2 A"
string(7) "loop2 B"
string(7) "loop2 C"
string(7) "loop1 B"
string(7) "loop2 A"
string(7) "loop2 B"
string(7) "loop2 C"
string(7) "loop1 C"
string(7) "loop2 A"
string(7) "loop2 B"
string(7) "loop2 C"