Оба этих интерфейса определяют только один метод
public operator fun iterator(): Iterator<T>
В документации написано Sequence
, что нужно лениться. Но разве не Iterable
ленив (если не подкреплен Collection
)?
Оба этих интерфейса определяют только один метод
public operator fun iterator(): Iterator<T>
В документации написано Sequence
, что нужно лениться. Но разве не Iterable
ленив (если не подкреплен Collection
)?
Ответы:
Ключевое различие заключается в семантике и реализации функций расширения stdlib для Iterable<T>
и Sequence<T>
.
Для Sequence<T>
функций расширения там, где это возможно, работают лениво, аналогично промежуточным операциям Java Streams . Например, Sequence<T>.map { ... }
возвращает другой Sequence<R>
и фактически не обрабатывает элементы, пока не будет вызвана операция терминала, такая как toList
или fold
.
Рассмотрим этот код:
val seq = sequenceOf(1, 2)
val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
print("before sum ")
val sum = seqMapped.sum() // terminal
Он печатает:
before sum 1 2
Sequence<T>
предназначен для ленивого использования и эффективной конвейерной обработки, когда вы хотите максимально сократить объем работы, выполняемой в терминальных операциях, как и Java Streams. Однако лень приводит к некоторым накладным расходам, что нежелательно для обычных простых преобразований небольших коллекций и делает их менее производительными.
В общем, нет хорошего способа определить, когда это необходимо, поэтому в Kotlin stdlib "лень" делается явной и извлекается в Sequence<T>
интерфейс, чтобы Iterable
по умолчанию не использовать ее во всех s.
Для Iterable<T>
, напротив, функции расширения с промежуточной операцией семантикой работы с нетерпением, обрабатывать детали сразу и вернуть другие Iterable
. Например, Iterable<T>.map { ... }
возвращает List<R>
с результатами сопоставления в нем.
Эквивалентный код для Iterable:
val lst = listOf(1, 2)
val lstMapped: List<Int> = lst.map { print("$it "); it * it }
print("before sum ")
val sum = lstMapped.sum()
Это распечатывает:
1 2 before sum
Как сказано выше, Iterable<T>
по умолчанию не ленив, и это решение хорошо себя показывает: в большинстве случаев у него хорошая локальность ссылки, что позволяет использовать кеш-память процессора, прогнозирование, предварительную выборку и т. Д., Так что даже многократное копирование коллекции по-прежнему работает хорошо. достаточно и лучше работает в простых случаях с небольшими коллекциями.
Если вам нужен больший контроль над конвейером оценки, есть явное преобразование в ленивую последовательность с Iterable<T>.asSequence()
функцией.
map
, filter
и другие, не несут достаточно информации для принятия решения, кроме как из исходного типа коллекции, и, поскольку большинство коллекций также являются Iterable, это не хороший маркер для «лениться», потому что это обычно ВЕЗДЕ. lazy должен быть явным, чтобы быть в безопасности.
Завершение ответа горячей клавиши:
Важно заметить, как Sequence и Iterable повторяются в ваших элементах:
Пример последовательности:
list.asSequence().filter { field ->
Log.d("Filter", "filter")
field.value > 0
}.map {
Log.d("Map", "Map")
}.forEach {
Log.d("Each", "Each")
}
Результат журнала:
фильтр - Карта - Каждый; фильтр - Карта - Каждый
Итерационный пример:
list.filter { field ->
Log.d("Filter", "filter")
field.value > 0
}.map {
Log.d("Map", "Map")
}.forEach {
Log.d("Each", "Each")
}
фильтр - фильтр - Карта - Карта - Каждый - Каждый
Iterable
сопоставляется сjava.lang.Iterable
интерфейсом вJVM
и реализуется часто используемыми коллекциями, такими как List или Set. Функции расширения коллекции на них быстро оцениваются, что означает, что все они немедленно обрабатывают все элементы во входных данных и возвращают новую коллекцию, содержащую результат.Вот простой пример использования функций сбора данных для получения имен первых пяти человек в списке, возраст которых составляет не менее 21 года:
val people: List<Person> = getPeople() val allowedEntrance = people .filter { it.age >= 21 } .map { it.name } .take(5)
Целевая платформа: JVMRunning on kotlin v. 1.3.61 Во-первых, проверка возраста выполняется для каждого отдельного человека в списке, а результат помещается в новый список. Затем выполняется сопоставление с их именами для каждого человека, который остался после оператора фильтра, и попадает в еще один новый список (теперь это a
List<String>
). Наконец, создается последний новый список, содержащий первые пять элементов предыдущего списка.Напротив, Sequence - это новая концепция в Kotlin, представляющая лениво оцениваемую коллекцию значений. Те же расширения коллекции доступны для
Sequence
интерфейса, но они немедленно возвращают экземпляры Sequence, которые представляют обработанное состояние даты, но без фактической обработки каких-либо элементов. Чтобы начать обработку,Sequence
необходимо завершить обработку оператором терминала, это в основном запрос к последовательности для материализации данных, которые она представляет в некоторой конкретной форме. Примеры включаютtoList
,toSet
иsum
, если упомянуть лишь некоторые из них. Когда они вызываются, будет обработано только минимально необходимое количество элементов для получения требуемого результата.Преобразовать существующую коллекцию в последовательность довольно просто, вам просто нужно использовать
asSequence
расширение. Как упоминалось выше, вам также необходимо добавить оператор терминала, иначе Sequence никогда не будет выполнять никакой обработки (опять же, ленивый!).
val people: List<Person> = getPeople() val allowedEntrance = people.asSequence() .filter { it.age >= 21 } .map { it.name } .take(5) .toList()
Целевая платформа: JVMRunning on kotlin v. 1.3.61 В этом случае каждый экземпляр Person в последовательности проверяется на свой возраст, если они проходят, их имена извлекаются, а затем добавляются в список результатов. Это повторяется для каждого человека в исходном списке, пока не будет найдено пять человек. На этом этапе функция toList возвращает список, а остальные участники
Sequence
не обрабатываются.Есть еще кое-что, на что способна Последовательность: она может содержать бесконечное количество элементов. С этой точки зрения логично, что операторы работают так же, как и они - оператор бесконечной последовательности никогда не сможет вернуться, если он будет выполнять свою работу с энтузиазмом.
В качестве примера, вот последовательность, которая будет генерировать столько степеней двойки, сколько требуется оператору терминала (игнорируя тот факт, что это может быстро переполниться):
generateSequence(1) { n -> n * 2 } .take(20) .forEach(::println)
Вы можете найти больше здесь .
Java
(в основномGuava
) фанатов