В отличие от C # IEnumerable
, где конвейер выполнения может выполняться столько раз, сколько мы хотим, в Java поток может быть «повторен» только один раз.
Любой вызов терминальной операции закрывает поток, делая его непригодным для использования. Эта «особенность» отнимает много энергии.
Я думаю, причина этого не техническая. Какие конструктивные соображения стояли за этим странным ограничением?
Изменить: чтобы продемонстрировать, о чем я говорю, рассмотрим следующую реализацию быстрой сортировки в C #:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
if (!ints.Any()) {
return Enumerable.Empty<int>();
}
int pivot = ints.First();
IEnumerable<int> lt = ints.Where(i => i < pivot);
IEnumerable<int> gt = ints.Where(i => i > pivot);
return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}
Теперь, чтобы быть уверенным, я не защищаю, что это хорошая реализация быстрой сортировки! Тем не менее, это прекрасный пример выразительной силы лямбда-выражения в сочетании с операцией потока.
И это не может быть сделано в Java! Я даже не могу спросить поток, пуст ли он, не сделав его непригодным для использования.
источник
IEnumerable
java.io.*
Ответы:
У меня есть некоторые воспоминания о ранней разработке Streams API, которые могут пролить свет на обоснование дизайна.
Еще в 2012 году мы добавляли лямбда-выражения в язык и хотели, чтобы набор операций, ориентированный на коллекции или «объемные данные», был запрограммирован с использованием лямбда-выражений, которые облегчили бы параллелизм. Идея лениво связывать операции была хорошо обоснована к этому моменту. Мы также не хотели, чтобы промежуточные операции сохраняли результаты.
Основными вопросами, которые нам нужно было решить, было то, как объекты в цепочке выглядели в API и как они подключались к источникам данных. Источниками часто являлись коллекции, но мы также хотели поддерживать данные, поступающие из файла или сети, или данные, генерируемые на лету, например, из генератора случайных чисел.
Существовало много влияний существующих работ на дизайн. Среди наиболее влиятельных были библиотека Google Guava и библиотека коллекций Scala. (Если кто -то удивляется о влиянии из гуавы, обратите внимание , что Кевин Bourrillion , гуавы ведущий разработчик, был на JSR-335 Lambda . Экспертной группы) В коллекции Scala, мы нашли этот разговор по Одерски быть особый интерес: перспективную Проверка коллекций Scala: от изменчивых до постоянных и параллельных . (Стэнфорд EE380, 1 июня 2011 г.)
Наш прототип в то время был основан на
Iterable
. Знакомые операцииfilter
,map
и так далее были расширение ( по умолчанию) методы наIterable
. Вызов одного добавил операцию в цепочку и вернул другойIterable
. Терминальная операция вродеcount
бы вызоветiterator()
цепочку к источнику, и операции будут реализованы в итераторе каждого этапа.Поскольку это Iterables, вы можете вызывать
iterator()
метод более одного раза. Что должно произойти потом?Если источником является коллекция, это в основном работает нормально. Коллекции являются Итерируемыми, и каждый вызов
iterator()
создает отдельный экземпляр Итератора, который не зависит от каких-либо других активных экземпляров, и каждый обходит коллекцию независимо. Отлично.А что, если источник однократный, как чтение строк из файла? Возможно, первый итератор должен получить все значения, но второй и последующие должны быть пустыми. Возможно, значения должны чередоваться среди итераторов. Или, может быть, каждый итератор должен получить все одинаковые значения. Тогда, что если у вас есть два итератора, и один становится дальше другого? Кто-то должен будет буферизовать значения во втором Итераторе, пока они не будут прочитаны. Хуже того, что если вы получите один итератор и прочитаете все значения, и только тогда получите второй итератор. Откуда берутся ценности? Требуется ли их буферизация на случай, если кто-то захочет второго итератора?
Очевидно, что использование нескольких итераторов в одном источнике вызывает много вопросов. У нас не было хороших ответов для них. Мы хотели последовательного, предсказуемого поведения для того, что произойдет, если вы позвоните
iterator()
дважды. Это подтолкнуло нас к запрету нескольких обходов, сделав трубопроводы одним выстрелом.Мы также наблюдали, как другие сталкивались с этими проблемами. В JDK большинство Iterables являются коллекциями или подобными коллекциям объектами, которые допускают многократный обход. Это нигде не указано, но, казалось, неписаное ожидание, что Iterables допускает многократный обход. Заметным исключением является интерфейс NIO DirectoryStream . Его спецификация включает в себя это интересное предупреждение:
[полужирный в оригинале]
Это казалось необычным и достаточно неприятным, так что мы не хотели создавать целую кучу новых итераций, которые могли бы быть разовыми. Это оттолкнуло нас от использования Iterable.
Примерно в это же время появилась статья Брюса Эккеля, в которой рассказывалось о проблемах, которые он испытывал со Скалой. Он написал этот код:
Это довольно просто. Он разбирает строки текста на
Registrant
объекты и выводит их дважды. За исключением того, что он на самом деле печатает их только один раз. Оказывается, он думал, чтоregistrants
это коллекция, хотя на самом деле это итератор. При втором вызовеforeach
встречается пустой итератор, из которого все значения были исчерпаны, поэтому он ничего не печатает.Такой опыт убедил нас в том, что очень важно иметь четко предсказуемые результаты при попытке множественного обхода. Он также подчеркнул важность разграничения ленивых конвейерных структур от реальных коллекций, в которых хранятся данные. Это, в свою очередь, привело к разделению ленивых конвейерных операций на новый интерфейс Stream и сохранению только активных, мутативных операций непосредственно в коллекциях. Брайан Гетц объяснил причины этого.
Как насчет разрешения множественного обхода для конвейеров на основе сбора, но запрета его для конвейеров не на основе сбора? Это противоречиво, но разумно. Если вы читаете значения из сети, вы, конечно, не сможете снова их просмотреть. Если вы хотите пройти их несколько раз, вы должны явно включить их в коллекцию.
Но давайте рассмотрим возможность множественного обхода из конвейеров на основе коллекций. Допустим, вы сделали это:
(
into
Операция теперь пишетсяcollect(toList())
.)Если источник является коллекцией, то первый
into()
вызов создаст цепочку итераторов обратно к источнику, выполнит операции конвейера и отправит результаты в место назначения. Второй вызовinto()
создаст еще одну цепочку итераторов и снова выполнит конвейерные операции . Это, очевидно, не так, но имеет эффект повторного выполнения всех операций фильтра и отображения для каждого элемента. Я думаю, что многие программисты были бы удивлены таким поведением.Как я упоминал выше, мы разговаривали с разработчиками Guava. Одна из замечательных вещей, которые у них есть, это кладбище идей, где они описывают функции, которые они решили не реализовывать, вместе с причинами. Идея ленивых коллекций звучит довольно круто, но вот что они должны сказать по этому поводу. Рассмотрим
List.filter()
операцию, которая возвращаетList
:Чтобы взять конкретный пример, какова стоимость
get(0)
илиsize()
в списке? Для часто используемых классов, такихArrayList
как O (1). Но если вы вызываете один из них в лениво отфильтрованном списке, он должен запустить фильтр над вспомогательным списком, и вдруг эти операции выполняются O (n). Хуже того, он должен пересекать список поддержки на каждой операции.Это казалось нам слишком большой ленью. Одно дело настроить некоторые операции и отложить фактическое выполнение до тех пор, пока вы не начнете. Другое дело - настроить все так, чтобы скрыть потенциально большое количество повторных вычислений.
Предлагая запретить нелинейные потоки или потоки «без повторного использования», Пол Сандос описал потенциальные последствия их разрешения как вызывающие «неожиданные или сбивающие с толку результаты». Он также упомянул, что параллельное выполнение сделает все еще сложнее. Наконец, я бы добавил, что конвейерная операция с побочными эффектами может привести к трудным и неясным ошибкам, если операция была неожиданно выполнена многократно или, по крайней мере, в другое число раз, чем ожидал программист. (Но Java-программисты не пишут лямбда-выражения с побочными эффектами, не так ли?
Таким образом, это является основным обоснованием разработки API Java 8 Streams, которая допускает обход в один прием и требует строго линейного (без разветвления) конвейера. Он обеспечивает согласованное поведение для нескольких различных потоковых источников, четко отделяет ленивые от активных операций и обеспечивает простую модель выполнения.
Что касается
IEnumerable
, я далеко не эксперт по C # и .NET, поэтому я был бы признателен за то, чтобы меня исправили (осторожно), если я сделаю какие-то неправильные выводы. Однако оказывается, чтоIEnumerable
множественные обходы позволяют вести себя по-разному с разными источниками; и это допускает разветвленную структуру вложенныхIEnumerable
операций, что может привести к некоторому значительному пересчету. Хотя я понимаю, что разные системы делают разные компромиссы, это две характеристики, которых мы стремились избежать при разработке API Java 8 Streams.Пример быстрой сортировки, данный ОП, интересен, озадачивает, и, к сожалению, несколько ужасает. Вызов
QuickSort
принимаетIEnumerable
и возвращаетIEnumerable
, так что сортировка фактически не выполняется, покаIEnumerable
не пройден финал . Однако, похоже, что вызов делает построение древовидной структуры,IEnumerables
которая отражает разделение, которое бы выполняла быстрая сортировка, фактически не делая этого. (В конце концов, это ленивое вычисление.) Если источник имеет N элементов, дерево будет иметь N элементов шириной в самом широком смысле и глубину lg (N).Мне кажется - и еще раз, я не эксперт по C # или .NET - что это приведет к тому, что некоторые вызовы безобидного вида, такие как выбор с помощью pivot
ints.First()
, будут дороже, чем они выглядят. На первом уровне, конечно, это O (1). Но рассмотрим раздел глубоко в дереве, с правого края. Чтобы вычислить первый элемент этого раздела, весь источник должен быть пройден, операция O (N). Но так как разделы выше ленивы, они должны быть пересчитаны, требуя O (LG N) сравнения. Таким образом, выбор оси будет операцией O (N lg N), которая так же дорога, как и весь вид.Но мы на самом деле не сортируем, пока не пройдем возвращенное
IEnumerable
. В стандартном алгоритме быстрой сортировки каждый уровень разделения удваивает количество разделений. Каждый раздел имеет только половину размера, поэтому каждый уровень остается на уровне сложности O (N). Дерево разделов имеет высоту O (LG N), поэтому общая работа составляет O (N LG N).С деревом ленивых IEnumerables, в нижней части дерева есть N разделов. Вычисление каждого раздела требует прохождения N элементов, каждый из которых требует сравнения lg (N) вверх по дереву. Для вычисления всех разделов в нижней части дерева требуется O (N ^ 2 lg N) сравнений.
(Это правильно? Я с трудом могу в это поверить. Кто-нибудь, пожалуйста, проверьте это для меня.)
В любом случае, действительно здорово, что
IEnumerable
этот способ можно использовать для построения сложных структур вычислений. Но если это действительно увеличивает вычислительную сложность настолько, насколько я думаю, то, казалось бы, программирование таким способом - это то, чего следует избегать, если только вы не будете чрезвычайно осторожны.источник
ints
: «Возможное многократное перечисление IEnumerable». Использование одного и того жеIEenumerable
более одного раза является подозрительным и его следует избегать. Я также хотел бы указать на этот вопрос (на который я ответил), который показывает некоторые предостережения с подходом .Net (помимо низкой производительности): List <T> и IEnumerable разницаЗадний план
В то время как вопрос кажется простым, фактический ответ требует некоторого фона, чтобы иметь смысл. Если вы хотите перейти к заключению, прокрутите вниз ...
Выберите точку сравнения - основные функции
Используя базовые концепции, концепция C #
IEnumerable
более тесно связана с JavaIterable
, которая может создавать столько итераторов, сколько вы хотите.IEnumerables
создатьIEnumerators
. JavaIterable
создатьIterators
История каждой концепции похожа, в том , что как
IEnumerable
иIterable
у основной мотивация , чтобы позволить «для-каждому» стилю зацикливания над членами сбора данных. Это упрощение, так как они оба допускают нечто большее, и они также достигли этой стадии через различные прогрессии, но это - существенная общая черта, несмотря на это.Давайте сравним эту особенность: если в обоих языках класс реализует
IEnumerable
/Iterable
, то этот класс должен реализовывать хотя бы один метод (для C # этоGetEnumerator
и для Java этоiterator()
). В каждом случае экземпляр, возвращаемый из этого (IEnumerator
/Iterator
), позволяет получить доступ к текущим и последующим элементам данных. Эта функция используется в синтаксисе для каждого языка.Выберите точку сравнения - расширенные функциональные возможности
IEnumerable
в C # был расширен, чтобы позволить ряд других возможностей языка (в основном, связанных с Linq ). Добавленные функции включают выборки, проекции, агрегации и т. Д. Эти расширения имеют сильную мотивацию от использования в теории множеств, аналогично понятиям SQL и реляционной базы данных.В Java 8 также были добавлены функциональные возможности, позволяющие получить степень функционального программирования с использованием Streams и Lambdas. Обратите внимание, что потоки Java 8 мотивируются не теорией множеств, а функциональным программированием. Несмотря на это, есть много параллелей.
Итак, это второй момент. Усовершенствования, внесенные в C #, были реализованы как расширение
IEnumerable
концепции. В Java, однако, сделанные улучшения были реализованы путем создания новых базовых концепций Lambdas и Streams, а затем также создания относительно тривиального способа преобразования изIterators
иIterables
в Streams, и наоборот.Таким образом, сравнение IEnumerable с концепцией Java в Stream является неполным. Вам нужно сравнить его с объединенными API потоков и коллекций в Java.
В Java потоки не совпадают с итерациями или итераторами
Потоки не предназначены для решения проблем так же, как итераторы:
С помощью
Iterator
вы получаете значение данных, обрабатываете его, а затем получаете другое значение данных.В Streams вы объединяете последовательность функций вместе, затем передаете входное значение в поток и получаете выходное значение из объединенной последовательности. Обратите внимание, что в терминах Java каждая функция инкапсулирована в одном
Stream
экземпляре. API-интерфейс Streams позволяет связывать последовательностьStream
экземпляров таким образом, чтобы связать последовательность выражений преобразования.Чтобы завершить
Stream
концепцию, вам нужен источник данных для подачи потока и функция терминала, которая потребляет поток.Способ, которым вы вводите значения в поток, на самом деле может быть от
Iterable
, но самаStream
последовательность не являетсяIterable
, это составная функция.A
Stream
также должен быть ленивым в том смысле, что он работает только тогда, когда вы запрашиваете у него значение.Обратите внимание на следующие важные предположения и особенности потоков:
Stream
в Java - это механизм преобразования, он преобразует элемент данных в одном состоянии в другое состояние.Сравнение C #
Если учесть, что поток Java является лишь частью системы снабжения, потока и сбора, а потоки и итераторы часто используются вместе с коллекциями, то неудивительно, что трудно соотнести те же понятия, которые почти все встроено в единую
IEnumerable
концепцию в C #.Части IEnumerable (и близкие связанные концепции) очевидны во всех концепциях Java Iterator, Iterable, Lambda и Stream.
Есть небольшие вещи, которые могут сделать концепции Java, которые сложнее в IEnumerable, и наоборот.
Вывод
Добавление потоков дает вам больше возможностей при решении проблем, которые справедливо классифицировать как «повышение мощности», а не «уменьшение», «отмена» или «ограничение».
Почему Java Streams разовые?
Этот вопрос ошибочен, потому что потоки - это последовательности функций, а не данные. В зависимости от источника данных, который передает поток, вы можете сбросить источник данных и передать тот же или другой поток.
В отличие от C # IEnumerable, где конвейер выполнения может выполняться столько раз, сколько мы хотим, в Java поток может быть «повторен» только один раз.
Сравнение
IEnumerable
сStream
ошибочным. Контекст, который вы используете, чтобы сказать,IEnumerable
может быть выполнен столько раз, сколько вы хотите, лучше всего по сравнению с JavaIterables
, который может повторяться столько раз, сколько вы хотите. JavaStream
представляет собой подмножествоIEnumerable
концепции, а не подмножество, которое предоставляет данные и, следовательно, не может быть «перезапущено».Любой вызов терминальной операции закрывает поток, делая его непригодным для использования. Эта «особенность» отнимает много энергии.
Первое утверждение в некотором смысле верно. Заявление «отнимает власть» - нет. Вы все еще сравниваете потоки это IEnumerables. Терминальная операция в потоке похожа на условие break в цикле for. Вы всегда можете иметь другой поток, если хотите, и если вы можете повторно предоставить данные, которые вам нужны. Опять же, если вы считаете, что
IEnumerable
это больше похоже наIterable
, для этого утверждения Java делает это просто отлично.Я думаю, причина этого не техническая. Каковы были конструктивные соображения, стоящие за этим странным ограничением?
Причина техническая, и по той простой причине, что поток является подмножеством того, что он думает. Подмножество потока не контролирует подачу данных, поэтому следует сбросить подачу, а не поток. В этом контексте это не так странно.
Пример быстрой сортировки
Ваш пример быстрой сортировки имеет подпись:
Вы рассматриваете ввод
IEnumerable
как источник данных:Кроме того, возвращаемое значение
IEnumerable
также является источником данных, и, поскольку это операция сортировки, порядок этих значений является значительным. Если вы считаете, что Java-Iterable
класс подходит для этого, в частности, дляList
специализацииIterable
, поскольку List является источником данных, который имеет гарантированный порядок или итерацию, то эквивалентный Java-код вашему коду будет:Обратите внимание, что есть ошибка (которую я воспроизвел) в том, что сортировка не обрабатывает повторяющиеся значения изящно, это сортировка «уникальное значение».
Также обратите внимание, как в Java-коде используются data source (
List
) и потоковые концепции в разных точках, и что в C # эти две «личности» могут быть выражены простоIEnumerable
. Кроме того, хотя я использовалList
базовый тип, я мог бы использовать более общийCollection
, и с небольшим преобразованием итератора в поток я мог бы использовать еще более общийIterable
источник
Stream
- это понятие момента времени, а не «операция цикла» .... (продолжение)f(x)
. Поток инкапсулирует функцию, он не инкапсулирует данные, которые проходят черезIEnumerable
может также предоставлять случайные значения, быть несвязанным и становиться активным до того, как данные существуют.IEnumerable<T>
будут представлять конечную коллекцию, которая может повторяться несколько раз. Некоторые вещи, которые являются итеративными, но не удовлетворяют этим условиям, реализуются,IEnumerable<T>
потому что никакой другой стандартный интерфейс не отвечает требованиям, но методы, которые ожидают, что конечные коллекции, которые могут быть повторены несколько раз, склонны к сбою, если даны итеративные вещи, которые не соответствуют этим условиям ,quickSort
пример мог бы быть намного проще, если бы он возвращал aStream
; это спасло бы два.stream()
звонка и один.collect(Collectors.toList())
звонок. Если вы заменитеCollections.singleton(pivot).stream()
сStream.of(pivot)
кодом становится почти читаемым ...Stream
s строятся вокругSpliterator
s, которые являются изменяемыми объектами с состоянием. У них нет действия «перезагрузки», и, фактически, требование поддержать такое действие перемотки «отнимает много сил». Как быRandom.ints()
обрабатывать такой запрос?С другой стороны, для
Stream
s, которые имеют прослеживаемое происхождение, легко создать эквивалентStream
для повторного использования. Просто поместите шаги, сделанные для созданияStream
многоразового метода. Имейте в виду, что повторение этих шагов не является дорогостоящей операцией, поскольку все эти шаги являются ленивыми операциями; фактическая работа начинается с работы терминала и в зависимости от фактической работы терминала может выполняться совершенно другой код.Вы, автор такого метода, можете указать, что означает вызов метода дважды: воспроизводит ли он точно такую же последовательность, как это делают потоки, созданные для неизмененного массива или коллекции, или создает поток с похожая семантика, но разные элементы, такие как поток случайных целых чисел или поток строк ввода с консоли и т. д.
Кстати, чтобы избежать путаницы, терминал операция потребляет
Stream
, который отличается от закрытияStream
как вызовclose()
на поток делает (который необходим для потоков , имеющие ассоциированные ресурсы , такие как, например , производстваFiles.lines()
).Кажется, что большая путаница проистекает из ошибочного сравнения
IEnumerable
сStream
. AnIEnumerable
представляет возможность предоставить фактическоеIEnumerator
, так что это какIterable
в Java. Напротив, aStream
является своего рода итератором и сопоставим с,IEnumerator
поэтому неправильно утверждать, что этот тип данных может использоваться несколько раз в .NET, поддержка дляIEnumerator.Reset
которого необязательна. В обсуждаемых здесь примерах скорее используется тот факт, что anIEnumerable
может использоваться для получения новыхIEnumerator
s, и это также работает с JavaCollection
; Вы можете получить новыйStream
. Если разработчики Java решили добавитьStream
операцииIterable
напрямую, промежуточные операции возвращают другоеIterable
, это было действительно сопоставимо, и это могло бы работать так же.Однако разработчики решили против этого и решение обсуждается в этом вопросе . Самым большим моментом является путаница в нетерпеливых операциях Collection и отложенных операциях Stream. Глядя на .NET API, я (да, лично) нахожу это оправданным. Несмотря на то, что это выглядит разумно, если смотреть
IEnumerable
отдельно, в конкретной коллекции будет много методов, непосредственно манипулирующих этой коллекцией, и множество методов, возвращающих ленивый типIEnumerable
, в то время как особая природа метода не всегда интуитивно распознается. Худший пример, который я нашел (в течение нескольких минут, которые я посмотрел на него), этоList.Reverse()
чье имя в точности совпадает с именем унаследованного (это правильный конец для методов расширения?)Enumerable.Reverse()
При совершенно противоположном поведении.Конечно, это два разных решения. Первый, который делает
Stream
тип отличным отIterable
/,Collection
и второй, чтобы сделатьStream
своего рода одноразовый итератор, а не другой тип итерируемого. Но эти решения были приняты вместе, и может быть так, что разделение этих двух решений никогда не рассматривалось. Он не был создан с учетом того, что можно сравнить с .NET.Фактическим решением разработки API было добавление улучшенного типа итератора
Spliterator
.Spliterator
s могут быть предоставлены старымиIterable
s (то есть, каким образом они были модифицированы) или совершенно новыми реализациями. ЗатемStream
был добавлен как высокоуровневый интерфейс к довольно низкому уровнюSpliterator
s. Вот и все. Вы можете обсудить, будет ли другой дизайн лучше, но он не продуктивен, он не изменится, учитывая то, как они спроектированы сейчас.Есть еще один аспект реализации, который вы должны рассмотреть.
Stream
s не являются неизменяемыми структурами данных. Каждая промежуточная операция может возвращать новыйStream
экземпляр, инкапсулирующий старую, но она также может вместо этого манипулировать своим собственным экземпляром и возвращать себя (что не препятствует выполнению даже обоих для одной и той же операции). Общеизвестными примерами являются операции, подобныеparallel
илиunordered
которые не добавляют еще один шаг, а манипулируют всем конвейером). Наличие такой изменчивой структуры данных и попыток повторного использования (или, что еще хуже, одновременного использования нескольких раз) не очень хорошо ...Для полноты вот ваш пример быстрой сортировки, переведенный на Java
Stream
API. Это показывает, что на самом деле это не «отнимает много сил».Может использоваться как
Вы можете написать его еще более компактным, как
источник
Stream
тогда как сброс исходных элементовSpliterator
подразумевается. И я совершенно уверен, что если бы это было возможно, на SO возникали вопросы, такие как: «Почему повторный вызовcount()
дваждыStream
дает разные результаты каждый раз» и т. Д.Stream
s до сих пор, связан с попыткой решить проблему путем многократного вызова терминальных операций (очевидно, в противном случае вы этого не замечаете), что привело к безмолвному решению, еслиStream
API разрешил это. с разными результатами на каждой оценке. Вот хороший пример .Я думаю, что между ними очень мало различий, если присмотреться.
На первый взгляд, это
IEnumerable
действительно многократно используемая конструкция:Тем не менее, компилятор фактически делает небольшую работу, чтобы помочь нам; он генерирует следующий код:
Каждый раз, когда вы фактически перебираете перечислимое, компилятор создает перечислитель. Перечислитель не может быть использован повторно; последующие вызовы
MoveNext
просто вернут false, и невозможно восстановить его в начале. Если вы хотите снова выполнить итерации по числам, вам нужно будет создать еще один экземпляр перечислителя.Чтобы лучше проиллюстрировать, что IEnumerable имеет (может иметь) ту же «особенность», что и поток Java, рассмотрим перечислимое устройство, источником чисел которого не является статическая коллекция. Например, мы можем создать перечислимый объект, который генерирует последовательность из 5 случайных чисел:
Теперь у нас есть код, очень похожий на предыдущий перечисляемый на основе массива, но со второй итерацией
numbers
:Во второй раз, когда мы проведем итерацию,
numbers
мы получим другую последовательность чисел, которую нельзя использовать в том же смысле. Или мы могли бы написатьRandomNumberStream
исключение для выброса, если вы попытаетесь повторить его несколько раз, делая перечисляемое фактически непригодным для использования (например, поток Java).Кроме того, что означает ваша быстрая сортировка на основе перечисления применительно к a
RandomNumberStream
?Вывод
Итак, самое большое отличие состоит в том, что .NET позволяет вам повторно использовать
IEnumerable
, неявно создавая новыйIEnumerator
в фоновом режиме всякий раз, когда ему потребуется доступ к элементам в последовательности.Это неявное поведение часто полезно (и «мощно», как вы заявляете), потому что мы можем многократно перебирать коллекцию.
Но иногда это неявное поведение может вызвать проблемы. Если ваш источник данных не является статичным или требует больших затрат (например, база данных или веб-сайт), то многие предположения о нем
IEnumerable
следует отбросить; повторное использование не так простоисточник
Можно обойти некоторые из защит «запустить один раз» в Stream API; например, мы можем избежать
java.lang.IllegalStateException
исключений (с сообщением «поток уже был обработан или закрыт») путем ссылки и повторного использованияSpliterator
(а неStream
непосредственно).Например, этот код будет работать без исключения:
Однако вывод будет ограничен
вместо того, чтобы повторять вывод дважды. Это связано с тем, что
ArraySpliterator
используемый в качествеStream
источника является сохраняющим состояние и сохраняет свою текущую позицию. Когда мы воспроизводим это,Stream
мы начинаем снова в конце.У нас есть несколько вариантов решения этой проблемы:
Мы могли бы использовать
Stream
метод создания без сохранения состояния, такой какStream#generate()
. Нам пришлось бы управлять состоянием извне в нашем собственном коде и выполнять сброс междуStream
«повторами»:Другое (немного лучшее, но не идеальное) решение этой проблемы заключается в написании нашего собственного
ArraySpliterator
(или аналогичногоStream
источника), который включает некоторую емкость для сброса текущего счетчика. Если бы мы использовали его для генерации,Stream
мы могли бы успешно воспроизвести их.Лучшее решение этой проблемы (на мой взгляд) состоит в создании новой копии любых состояний,
Spliterator
используемых вStream
конвейере, когда новые операторы вызываются вStream
. Это более сложный и сложный для реализации, но если вы не возражаете против использования сторонних библиотек, в циклоп-реакции естьStream
реализация, которая делает именно это. (Раскрытие информации: я ведущий разработчик этого проекта.)Это напечатает
как и ожидалось.
источник