Является ли "бесконечный" итератор плохим дизайном?
Обычно считается, что плохая практика обеспечивает Iterator
реализации, которые являются "бесконечными"; т.е. когда вызовы hasNext()
всегда (*) возвращают true?
Обычно я бы сказал "да", потому что вызывающий код мог вести себя беспорядочно, но в приведенной ниже реализации hasNext()
будет возвращать true, если вызывающий объект не удалит все элементы из списка, которые инициализировали итератор; то есть существует условие завершения. Считаете ли вы, что это законное использование Iterator
? Это, похоже, не нарушает контракт, хотя я полагаю, что можно было бы утверждать, что это неинтуитивно.
public class CyclicIterator<T> implements Iterator<T> {
private final List<T> l;
private Iterator<T> it;
public CyclicIterator<T>(List<T> l) {
this.l = l;
this.it = l.iterator();
}
public boolean hasNext() {
return !l.isEmpty();
}
public T next() {
T ret;
if (!hasNext()) {
throw new NoSuchElementException();
} else if (it.hasNext()) {
ret = it.next();
} else {
it = l.iterator();
ret = it.next();
}
return ret;
}
public void remove() {
it.remove();
}
}
(Pedantic) EDIT
Некоторые люди прокомментировали, как Iterator
можно использовать для генерации значений из неограниченной последовательности, такой как последовательность Фибоначчи. Однако в документации Java Iterator
указано, что Iterator:
Итератор над коллекцией.
Теперь вы можете утверждать, что последовательность Фибоначчи представляет собой бесконечную коллекцию, но в Java я бы приравнивал коллекцию к интерфейсу java.util.Collection
, который предлагает такие методы, как size()
, подразумевая, что коллекция должна быть ограничена. Поэтому допустимо ли использовать Iterator
как генератор значений из неограниченной последовательности?
Ответы
Ответ 1
Я думаю, что он полностью легитимный - Iterator
- это просто поток "материала". Почему поток обязательно должен быть ограничен?
Множество других языков (например, Scala) имеют концепцию неограниченных потоков, встроенных в них, и их можно повторить. Например, используя scalaz
scala> val fibs = (0, 1).iterate[Stream](t2 => t2._2 -> (t2._1 + t2._2)).map(_._1).iterator
fibs: Iterator[Int] = non-empty iterator
scala> fibs.take(10).mkString(", ") //first 10 fibonnacci numbers
res0: String = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
EDIT: В терминах принципа наименьшего удивления, я думаю, он полностью зависит от контекста. Например, что я ожидаю, что этот метод вернется?
public Iterator<Integer> fibonacciSequence();
Ответ 2
Вся точка Iterator
заключается в том, что она ленива, то есть она дает вам столько же объектов, сколько вы просите. Если пользователь запрашивает все объекты бесконечного Iterator
, это их проблема, а не ваша.
Ответ 3
В то время как я тоже считаю, что это законно, я хотел бы добавить, что такой Iterator
(или более точно: Iterable
, создающий такой Iterator
) не будет хорошо работать с расширенным for-loop ( aka for-each-loop):
for (Object o : myIterable) {
...
}
Поскольку код внутри цикла расширенного цикла не имеет прямого доступа к итератору, он не может вызывать remove()
на итераторе.
Итак, чтобы закончить цикл, он должен был бы выполнить одно из следующих действий:
- Получить доступ к внутреннему
List
Iterator
и удалить объекты напрямую, возможно, вызвав ConcurrentModificationException
- используйте
break
для выхода из цикла
- "использовать"
Exception
для выхода из цикла
- используйте
return
, чтобы выйти из цикла
Все, кроме последнего из этих альтернатив, не совсем лучший способ оставить расширенный цикл for.
"Обычный" для цикла (или любой другой цикл вместе с явной переменной Iterator
), конечно, будет работать нормально:
for (Iterator it = getIterator(); it.hasNext();) {
Object o = it.next()
...
}
Ответ 4
Это может быть семантика, но итератор должен усердно возвращать следующий элемент без учета того, когда он закончится. Окончание является побочным эффектом для коллекции, хотя это может показаться общим побочным эффектом.
Тем не менее, какая разница между бесконечным и коллекцией с 10 триллионами предметов? Вызывающий либо хочет их всех, либо нет. Пусть вызывающий абонент решает, сколько элементов вернуться или когда закончится.
Я бы не сказал, что вызывающий абонент не мог использовать конструкцию for-each. Он мог бы, пока он хочет все предметы.
Что-то в документации для коллекции вроде "может быть бесконечной коллекцией" было бы подходящим, но не "бесконечным итератором".
Ответ 5
Это совершенно законное использование - , если оно правильно документировано.
Использование имени CyclicIterator
является хорошей идеей, так как это указывает на то, что цикл на итераторе вполне может быть бесконечным, если случай выхода цикла не определен правильно.
Ответ 6
Бесконечный итератор очень полезен при создании бесконечных данных, например. линейные рекуррентные последовательности, такие как последовательность Фибоначчи.
Так что это совершенно нормально использовать.
Ответ 7
Да. Вам просто нужны разные критерии, чтобы остановить итерацию.
Концепция бесконечных списков очень распространена в ленивых оценочных языках, таких как Haskell, и приводит к совершенно различным программам.
Ответ 8
Собственно, Iterator
происходит от Iterable
, а не от Collection
, независимо от того, что говорит API JavaDoc. Последний расширяет первое, и поэтому он может генерировать Iterator
, но вполне разумно, чтобы Iterable
не был Collection
. На самом деле существует даже несколько классов Java, которые реализуют Iterable
, но не Collection
.
Теперь, что касается ожиданий... конечно, бесконечный итератор не должен быть доступен без четкого указания на то, что это происходит. Но иногда ожидания могут обманывать. Например, можно ожидать, что InputStream
всегда подходит к концу, но это действительно не является обязательным требованием. Аналогично, строки возврата Iterator
, считанные из такого потока, могут никогда не закончиться.
Ответ 9
Рассмотрим, есть ли у вас функция генератора. Итератор может быть разумным интерфейсом к генератору, или, может быть, не так, как вы заметили.
С другой стороны, у Python есть генераторы, которые заканчиваются.
Ответ 10
Я могу представить любое количество применений для бесконечного итератора. Например, как насчет программы, итерации через сообщения статуса, отправляемые другим сервером или фоновым процессом. В то время как в реальной жизни сервер, по-видимому, в конечном итоге остановится, с программной точки зрения он может просто продолжать чтение до тех пор, пока он не остановится чем-то, не связанным с тем, что итератор достигнет своего конца.
Это, безусловно, было бы необычным, и, как говорили другие, следует тщательно документировать. Но я бы не объявил это неприемлемым.
Ответ 11
Если у вас есть объект, который нуждается в итераторе, и вы должны дать ему бесконечный итератор, тогда все должно быть хорошо. Конечно, этот объект никогда не должен останавливать итератор. И , который является потенциально плохим дизайном.
Подумайте о музыкальном проигрывателе. Вы говорите ему, чтобы начать воспроизведение треков в режиме непрерывного воспроизведения, и он воспроизводит дорожки, пока вы не скажете, чтобы он остановился. Возможно, вы реализуете этот режим непрерывного воспроизведения, перетасовывая плейлист навсегда... пока пользователь не нажмет "стоп". В этом случае MusicPlayer
требуется итератор над списком воспроизведения, который может быть Collection<Track>
, навсегда. В этом случае вам понадобится либо CyclicIterator<Track>
, как вы построили, либо ShuffleIterator<Track>
, который случайно выбирает следующий трек и никогда не заканчивается. Пока что так хорошо.
MusicPlayer.play(Iterator<Track> playlist)
не волнует, закончится ли список воспроизведения или нет. Он работает как с итератором по списку треков (проигрывается до конца, затем останавливается), так и с помощью ShuffleIterator<Track>
по списку треков (выбирайте случайный трек навсегда). Все по-прежнему хорошо.
Что делать, если кто-то попытался написать MusicPlayer.playContinuously()
- который ожидает воспроизведение треков "навсегда" (пока кто-то не нажмет "остановить" ), но принимает параметр Iterator<Track>
в качестве параметра? Это будет проблемой дизайна. Очевидно, что playContinuously()
нуждается в бесконечном Iterator
, и если он принимает обычный Iterator
в качестве своего параметра, то , который будет плохим дизайном. Например, нужно было бы проверить hasNext()
и обработать случай, когда он возвращает false
, хотя это никогда не должно происходить. Bad.
Итак, реализуйте все бесконечные итераторы, которые вы хотите, до тех пор, пока их клиенты не зависят от окончания итераторов. Как будто ваш клиент зависит от продолжения итератора, то не дает ему простой Iterator
.
Это должно быть очевидно, но, похоже, это не так.