PHP SeekableIterator: поймать OutOfBoundsException или проверить метод valid()?

Поэтому я не уверен, является ли это ошибочным дизайном с PHP или есть понятная логика для обработки противоречивых результатов для одного и того же интерфейса.

Интерфейс SeekableIterator имеет два метода (seek и valid), которые либо конфликтуют друг с другом, либо должны работать последовательно друг с другом, но я вижу оба.

В документации к интерфейсу сказано, что seek должно генерировать исключение класса OutOfBoundsException, но это, по-видимому, сводит на нет полезность valid, если только позиция итератора не обновляется (заставляя valid возвращать false) перед генерацией исключения (которое, по-видимому, должно быть перехвачено).

Три тестовых примера

Пример 1.

Пользовательский класс, реализующий SeekableIterator, как показано в примере в документах:

Класс:

class MySeekableIterator implements SeekableIterator {

    private $position;

    private $array = array(
        "first element",
        "second element",
        "third element",
        "fourth element"
    );

    /* Method required for SeekableIterator interface */

    public function seek($position) {
        if (!isset($this->array[$position])) {
            throw new OutOfBoundsException("invalid seek position ($position)");
        }

        $this->position = $position;
    }

    /* Methods required for Iterator interface */

    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]);
    }
}

Пример 1. Тест:

echo PHP_EOL . "Custom Seekable Iterator seek Test" . PHP_EOL;

$it = new MySeekableIterator;

$it->seek(1);
try {
    $it->seek(10);
    echo $it->key() . PHP_EOL;
    echo "Is valid? " . (int) $it->valid() . PHP_EOL;
} catch (OutOfBoundsException $e) {
    echo $e->getMessage() . PHP_EOL;
    echo $it->key() . PHP_EOL; // outputs previous position (1)
    echo "Is valid? " . (int) $it->valid() . PHP_EOL;
}

Выход теста 1:

Custom Seekable Iterator seek Test
invalid seek position (10)
1
Is valid? 1

Пример 2:

Использование родного ArrayIterator::seek

Тест 2 Код:

echo PHP_EOL . "Array Object Iterator seek Test" . PHP_EOL;

$array = array('1' => 'one',
               '2' => 'two',
               '3' => 'three');

$arrayobject = new ArrayObject($array);
$iterator = $arrayobject->getIterator();

$iterator->seek(1);
try {
    $iterator->seek(5);
    echo $iterator->key() . PHP_EOL;
    echo "Is valid? " . (int) $iterator->valid() . PHP_EOL;
} catch (OutOfBoundsException $e) {
    echo $e->getMessage() . PHP_EOL;
    echo $iterator->key() . PHP_EOL;  // outputs previous position (1)
    echo "Is valid? " . (int) $iterator->valid() . PHP_EOL;
}

Выход теста 2:

Array Object Iterator seek Test
Seek position 5 is out of range
1
Is valid? 1

Пример 3:

Использование родного DirectoryIterator::seek

Тест 3 Код:

echo PHP_EOL . "Directory Iterator seek Test" . PHP_EOL;

$dir_iterator = new DirectoryIterator(dirname(__FILE__));
$dir_iterator->seek(1);
try {
    $dir_iterator->seek(500);  // arbitrarily high seek position
    echo $dir_iterator->key() . PHP_EOL;
    echo "Is valid? " . (int) $dir_iterator->valid() . PHP_EOL;
} catch (OutOfBoundsException $e) {
    echo $e->getMessage() . PHP_EOL;
    echo $dir_iterator->key() . PHP_EOL;
    echo "Is valid? " . (int) $dir_iterator->valid() . PHP_EOL;
}

Выход теста 3:

Directory Iterator seek Test
90
Is valid? 0

Итак, как можно разумно ожидать узнать, следует ли использовать valid() для подтверждения действительной позиции после seek($position), а также предвидеть, что seek() может вызвать исключение вместо обновления позиции, так что valid() вернет true?


person Anthony    schedule 22.05.2015    source источник


Ответы (1)


Кажется, что метод directoryIterator::seek() здесь не реализован за исключением. Вместо этого он просто не вернет никакого значения, и пусть valid() обработает его.

Другой ваш пример, ArrayObject::seek() работает "правильно" и выдает OutOfBoundsException.

Причина проста: ArrayObject (и, скорее всего, большинство пользовательских реализаций тоже) будет знать заранее, сколько элементов она содержит, и, таким образом, может быстро проверить свои границы. Однако DirectoryIterator должен считывать объекты каталога с диска один за другим, чтобы достичь заданной позиции. Он делает это, буквально вызывая valid() и next() в цикле. По этой причине key() изменилось, а valid() возвращает 0.

Другие итераторы даже не будут касаться текущего состояния итератора и могут быстро решить, попадает ли ваш запрос в его диапазон или нет.

На заметку: если вы хотите искать позицию в DirectoryIterator в обратном направлении, он сначала сбросит итератор, а затем снова начнет итерацию каждого элемента. Итак, если вы находитесь на позиции 1000 и выполняете $it->seek(999), на самом деле он снова повторит 999 элементов.

ИМХО, DirectoryIterator не является хорошей реализацией интерфейса seekableIterator. Он предназначен для быстрого перехода к определенному элементу в итераторе, и ясно, что с помощью directoryIterator это невозможно. Вместо этого необходимо выполнить полную итерацию, результатом которой будет изменение состояния итератора.

Интерфейс seekableIterator полезен для filterIterators, которые что-то делают с диапазоном итератора. В SPL это только LimitIterator. Когда вы делаете:

$it = new ArrayIterator(range('a','z'));
$it = new LimitIterator($it, 5, 10));

Когда limitIterator обнаружит, что данный итератор реализовал интерфейс seekableIterator, он вызовет seek() для быстрого перехода к 5-му элементу, в противном случае он просто будет выполнять итерацию, пока не достигнет 5-го элемента.

Вывод: не используйте seekableIterator, когда вы не можете быстро перейти к позиции или проверить границы. В лучшем случае вы ничего не получите, в худшем вы получите итераторы, которые меняют состояние, не зная почему.

Чтобы ответить на ваш вопрос: seek() должен вызывать исключение, а не изменять состояние. directoryIterator (может быть, и некоторые другие) следует изменить, чтобы либо не реализовывать seekableIterator, либо узнать, сколько записей существует до seek() (но это не устраняет проблему «перемотки назад» при поиске назад).

person JayTaph    schedule 30.05.2015
comment
Очень мило спасибо. Лично я считаю, что создание исключения не должно быть нормой, но ваше объяснение того, почему directoryIterator является плохим вариантом использования seekableIterator, является веским контраргументом и указывает, почему его следует обрабатывать как особое исключение, а не переоснащать интерфейс для не принимать выброшенное исключение для интерфейса. - person Anthony; 01.06.2015
comment
В частности, я думаю, что создание исключения для этого конкретного интерфейса не должно быть нормой, поскольку это добавляет путаницы относительно того, когда полагаться на метод valid, а когда принимать исключение. Было бы более запутанным/повреждающим изменение состояния итератора путем создания исключения и обновления позиции, чтобы valid возвращало false? - person Anthony; 01.06.2015
comment
Нет. Я думаю, что это будет возможно, однако я не знаю, будет ли это считаться нарушением BC и вызовет ли какие-либо проблемы в пользовательском коде (хотя, если это произойдет, этот код, вероятно, слишком хрупок для начала). Возможно, это было бы хорошим исправлением для PHP7. - person JayTaph; 06.06.2015
comment
@Anthony Это на самом деле спроектированное поведение, поскольку оно описано в тестовый пример. Однако реализации должны генерировать исключение OutOfBoundsException, если позиция недоступна для поиска, и я думаю должно быть исключение здесь . - person bishop; 23.09.2015
comment
@Anthony Я открыл ошибку №70561, исправил и создал PR. Голосуйте! - person bishop; 23.09.2015
comment
@Anthony Это теперь исправлено! В PHP 5.6.15 и более поздних версиях DirectoryIterator выдает исключение, как и другие реализации SeekableIterator. - person bishop; 23.10.2015