简体   繁体   English

带有迭代器接口的嵌套 foreach

[英]nested foreach with iterator interface

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

This code give unexpected results, because only one link is visible.这段代码给出了意想不到的结果,因为只有一个链接是可见的。 But there are two items in $this->criteria.但是 $this->criteria 中有两个项目。

I explored the cause of the probleem.我调查了问题的原因。 In the function accommodationsLink is another foreach loop that works on the same criteria object在函数适应链接中是另一个 foreach 循环,它适用于相同的标准对象

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

$this->criteria and $criteria are the same object that implements the php Iterator interface. $this->criteria 和 $criteria 是实现 php Iterator 接口的同一个对象。 Is there a simple way to let this code work or are nested foreach loops not possible with php iterator interface?有没有一种简单的方法可以让这段代码工作,或者嵌套的 foreach 循环无法使用 php 迭代器接口?

I tried this with both plain arrays and PHP iterators.我用普通数组和 PHP 迭代器尝试了这个。 Unfortunately PHP iterators, since they're objects, work differently.不幸的是 PHP 迭代器,因为它们是对象,所以工作方式不同。 Objects are passed by reference while arrays are by-value.对象是按引用传递的,而数组是按值传递的。 So when the nested foreach reaches the end of the iterator the first foreach can't resume where it stopped because the internal pointer is set to the last element.因此,当嵌套的 foreach 到达迭代器的末尾时,第一个 foreach 无法从停止的位置恢复,因为内部指针设置为最后一个元素。

Consider the following example written using a plain PHP array:考虑以下使用普通 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";
    }
}

The above snippet produces the following output:上面的代码片段产生以下输出:

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

If we try the same thing with an iterator we get quite a different result.如果我们用迭代器尝试同样的事情,我们会得到完全不同的结果。 To avoid confusion I will use the ArrayIterator class so that everything is already implemented by the PHP guys and we don't end up using interfaces the wrong way.为了避免混淆,我将使用ArrayIterator类,以便 PHP 人员已经实现了所有内容,并且我们最终不会以错误的方式使用接口。 So no room for errors here, this is how iterators are implemented by them:所以这里没有错误的余地,这就是他们实现迭代器的方式:

$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";
    }
}

Output is:输出是:

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

As you can see the first foreach gets executed only once.如您所见,第一个 foreach 只执行一次。

A workaround could be implementing the SeekableIterator interface.解决方法可能是实现SeekableIterator接口。 It would let us use the seek() method to reset the internal pointer to its correct value.它将让我们使用seek()方法将内部指针重置为其正确值。 It's a bad practice in my opinion but if the PHP guys don't fix this thing I can't really say what it could be best.在我看来,这是一个不好的做法,但如果 PHP 人员不解决这个问题,我真的不能说它可能是最好的。 I'd probably avoid iterators from now on since they seem to behave differently from arrays which I think it's what people assume at first.从现在开始我可能会避免使用迭代器,因为它们似乎与数组的行为不同,我认为这是人们一开始所假设的。 So using them would make my application error prone because it could be that a dev in my team doesn't know about this and messes with the code.因此,使用它们会使我的应用程序容易出错,因为可能是我团队中的开发人员不知道这一点并弄乱了代码。

Follows an example with the SeekableIterator interface:遵循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);
}

Output is as anyone would expect:输出正如任何人所期望的那样:

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

All this happens because each foreach works on its own array copy.所有这一切的发生都是因为每个 foreach 都在自己的数组副本上工作。 Iterators, since they are objects, are passed by reference.迭代器,因为它们是对象,所以通过引用传递。 Therefore each foreach shares the same object.因此,每个 foreach 共享相同的对象。 The same thing happens if you try to unset an element in the nested foreach.如果您尝试在嵌套的 foreach 中取消设置元素,则会发生同样的事情。 The unset will increment the internal pointer.未设置将增加内部指针。 Then the execution reaches the end of the nested foreach and the internal pointer gets increased again.然后执行到达嵌套 foreach 的末尾,内部指针再次增加。 This means that with an unset we increase the internal pointer two times.这意味着在未设置的情况下,我们将内部指针增加了两倍。 The parent foreach will therefore skip an element.因此,父 foreach 将跳过一个元素。

My advice is, if you can't avoid iterators, be REALLY REALLY careful.我的建议是,如果你不能避免迭代器,真的要非常小心。 Always unit test them thoroughly.始终对它们进行彻底的单元测试。

Note: Code tested on PHP 5.6.14 and PHP 7.0.0 RC5.注意:代码在 PHP 5.6.14 和 PHP 7.0.0 RC5 上测试。

Well, the second foreach is going to call $iterator->reset() prior to running.好吧,第二个 foreach 将在运行之前调用$iterator->reset() So when the second foreach reaches the end of the iterator, the internal pointer is already at the end of the array...所以当第二个 foreach 到达迭代器的末尾时,内部指针已经在数组的末尾了……

It'd be like:它会是这样的:

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

Buy the time it gets to the $it->next() call in the outer loop, it's already invalid.购买它到达外循环中$it->next()调用的时间,它已经无效了。 So the next() call will "fail", and $it->valid() would return false.因此next()调用将“失败”,而$it->valid()将返回 false。

It's not a problem with iterators, it's a problem with the logic you're using.这不是迭代器的问题,而是您使用的逻辑的问题。 If you really must nest loops, then clone the iterator ( $subit = clone $it ) in the inner loop so you don't disturb the pointer...如果你真的必须嵌套循环,那么在内部循环中clone迭代器( $subit = clone $it ),这样你就不会干扰指针......

Edit: Example with cloning:编辑:克隆示例:

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

Or, using foreach (which is semantically equivalent):或者,使用 foreach(在语义上是等价的):

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

EDIT: after posting this I realised this will break badly if you do continue or break in the nested foreach.编辑:发布此内容后,我意识到如果您continuebreak嵌套的 foreach,这将严重break So this is probably not your desired solution.所以这可能不是您想要的解决方案。

As stated in other answers PHP foreach calls rewind at the beginning of the foreach loop and valid at the end of each iteration.如其他答案中所述,PHP foreach 在foreach循环开始时调用rewind并在每次迭代结束时valid So in the nested foreach iterator gets invalid and stays this way in parent foreach .所以在嵌套的foreach迭代器中变得无效并在父foreach保持这种方式。 Here is a hackish workaround that uses stack of pointers instead of a single pointer and makes this iterator behave like arrays in this case.这是一个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);
    }
}

output:输出:

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"

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM