【问题标题】:nested foreach with iterator interface带有迭代器接口的嵌套 foreach
【发布时间】:2011-03-25 05:52:15
【问题描述】:
<? foreach ($this->criteria as $key => $value): ?>
<li><?= $this->accommodationsLink($this->criteria, $key) ?></li>
<? endforeach ?>

此代码给出了意想不到的结果,因为只有一个链接可见。但是 $this->criteria 中有两个项目。

我探索了问题的原因。在函数 accommodationLink 中是另一个适用于相同条件对象的 foreach 循环

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

$this->criteria 和 $criteria 是实现 php Iterator 接口的同一个对象。有没有一种简单的方法可以让这段代码工作,或者使用 php 迭代器接口无法实现嵌套的 foreach 循环?

【问题讨论】:

    标签: php iterator foreach


    【解决方案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";
        }
    }
    

    上面的 sn-p 产生以下输出:

    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 中的元素,也会发生同样的事情。未设置将增加内部指针。然后执行到达嵌套 foreach 的末尾,内部指针再次增加。这意味着在未设置的情况下,我们将内部指针增加了两倍。因此,父 foreach 将跳过一个元素。

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

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

    【讨论】:

      【解决方案2】:

      编辑: 发布此消息后,我意识到如果您在嵌套的 foreach 中执行 continuebreak,这将严重破坏。所以这可能不是您想要的解决方案。

      如其他答案所述,PHP foreach 在 foreach 循环开始时调用 rewind,在每次迭代结束时调用 valid。所以在嵌套的foreach 迭代器中变得无效并在父foreach 中保持这种方式。这是一个骇人听闻的解决方法,它使用指针堆栈而不是单个指针,并使此迭代器在这种情况下表现得像数组。

      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"
      

      【讨论】:

        【解决方案3】:

        好吧,第二个 foreach 将在运行前调用 $iterator-&gt;reset()。所以当第二个foreach到达迭代器的末尾时,内部指针已经在数组的末尾了……

        应该是这样的:

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

        在外部循环中购买到$it-&gt;next() 调用的时间,它已经无效。所以next() 调用将“失败”,$it-&gt;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
            }
        }
        

        【讨论】:

        • 另一种方法是实现IteratorAggregate。如果碰巧实现迭代器的对象封装了大量数据,则克隆可能会占用大量内存。
        • @Artefacto 你应该根据你的评论写一个答案。
        猜你喜欢
        • 2015-08-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-04-07
        相关资源
        最近更新 更多