вложенный foreach с интерфейсом итератора - PullRequest
6 голосов
/ 04 августа 2010
<? foreach ($this->criteria as $key => $value): ?>
<li><?= $this->accommodationsLink($this->criteria, $key) ?></li>
<? endforeach ?>

Этот код дает неожиданные результаты, потому что видна только одна ссылка.Но в $ this-> критерии есть два пункта.

Я исследовал причину проблемы.В функции AccommodationingsLink есть еще один цикл foreach, который работает с одним и тем же объектом критериев

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

$ this-> критерии и $ критерии - это тот же объект, который реализует интерфейс итератора php.Есть ли простой способ позволить этому коду работать или вложенные циклы foreach невозможны с помощью итератора php?

Ответы [ 3 ]

2 голосов
/ 04 августа 2010

Хорошо, второй 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 голос
/ 13 января 2016

РЕДАКТИРОВАТЬ: после публикации я понял, что это плохо сломается, если вы выполните continue или break во вложенном foreach.Так что это, вероятно, не желаемое решение.

Как указано в других ответах PHP foreach вызывает rewind в начале цикла foreach и 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);
    }
}

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"
1 голос
/ 29 октября 2015

Я пробовал это как с простыми массивами, так и с итераторами 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.

...