Последовательности
Наряду со списками, стандартная библиотека Kotlin содержит ещё один тип контейнера — последовательности (Sequence<T>). Последовательности предлагают те же функции, что и Iterable, но реализуют другой подход к многоступенчатой обработке коллекций.
Когда обработка Iterable включает несколько шагов, они выполняются жадно: каждый шаг обработки завершается и возвращает свой результат — промежуточную коллекцию. Следующий шаг выполняется над этой коллекцией. В свою очередь, многоступенчатая обработка последовательностей выполняется лениво, когда это возможно: фактическое вычисление происходит только тогда, когда запрашивается результат всей цепочки обработки.
Порядок выполнения операций также отличается: Sequence выполняет все шаги обработки один за другим для каждого элемента. В свою очередь, Iterable завершает каждый шаг для всей коллекции, а затем переходит к следующему шагу.
Таким образом, последовательности позволяют избежать создания результатов промежуточных шагов, что повышает производительность всей цепочки обработки коллекций. Однако ленивый характер последовательностей добавляет некоторую издержку, которая может быть значительной при обработке меньших коллекций или выполнении более простых вычислений. Поэтому вы должны рассмотреть как Sequence , так и Iterable и решить, какой из них лучше подходит для вашего случая.
Создание
Из элементов
Чтобы создать последовательность, вызовите функцию sequenceOf(), перечислив элементы в качестве её аргументов.
val numbersSequence = sequenceOf("four", "three", "two", "one")
Из Iterable
Если у вас уже есть объект Iterable (например, List или Set), вы можете создать из него последовательность, вызвав asSequence().
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
Из функции
Ещё один способ создать последовательность — построить её с помощью функции, которая вычисляет её элементы. Чтобы построить последовательность на основе функции, вызовите generateSequence() с этой функцией в качестве аргумента. Необязательно, вы можете указать первый элемент как явное значение или результат вызова функции. Генерация последовательности останавливается, когда предоставленная функция возвращает null. Таким образом, последовательность в примере ниже бесконечна.
fun main() {
//sampleStart
val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList())
//println(oddNumbers.count()) // error: the sequence is infinite
//sampleEnd
}
Чтобы создать конечную последовательность с generateSequence(), предоставьте функцию, которая возвращает null после последнего необходимого элемента.
fun main() {
//sampleStart
val oddNumbersLessThan10 = generateSequence(1) { if (it < 8) it + 2 else null }
println(oddNumbersLessThan10.count())
//sampleEnd
}
Из частей
Наконец, есть функция, которая позволяет генерировать элементы последовательности по одному или частями произвольного размера — функция sequence(). Эта функция принимает лямбда-выражение, содержащее вызовы функций yield() и yieldAll(). Они возвращают элемент потребителю последовательности и приостанавливают выполнение sequence() до тех пор, пока потребитель не запросит следующий элемент. yield() принимает один элемент в качестве аргумента; yieldAll() может принимать объект Iterable, Iterator, или другую Sequence. Аргумент Sequence функции yieldAll() может быть бесконечным. Однако такой вызов должен быть последним: все последующие вызовы никогда не будут выполнены.
fun main() {
//sampleStart
val oddNumbers = sequence {
yield(1)
yieldAll(listOf(3, 5))
yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())
//sampleEnd
}
Операции над последовательностями
Операции над последовательностями можно классифицировать по следующим группам в зависимости от их требований к состоянию:
Бессостоятельные операции не требуют состояния и обрабатывают каждый элемент независимо, например,
map()илиfilter(). Бессостоятельные операции также могут потребовать небольшого постоянного объёма состояния для обработки элемента, например,take()илиdrop().Состоятельные операции требуют значительного объёма состояния, обычно пропорционального количеству элементов в последовательности.
Если операция над последовательностью возвращает другую последовательность, которая генерируется лениво, она называется промежуточной. В противном случае операция является конечной. Примерами конечных операций являются toList() или sum(). Элементы последовательности могут быть получены только с помощью конечных операций.
Последовательности можно итерировать несколько раз; однако некоторые реализации последовательностей могут ограничить себя однократной итерацией. Это специально упоминается в их документации.
Пример обработки последовательности
Давайте рассмотрим разницу между Iterable и Sequence на примере.
Iterable
Предположим, у вас есть список слов. Следующий код фильтрует слова, длина которых больше трёх символов, и выводит длины первых четырёх таких слов.
fun main() {
//sampleStart
val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)
//sampleEnd
}
При запуске этого кода вы увидите, что функции filter() и map() выполняются в том же порядке, в котором они появляются в коде. Сначала вы видите filter: для всех элементов, затем length: для элементов, оставшихся после фильтрации, а затем вывод двух последних строк.
Вот как происходит обработка списка:
Последовательность
Теперь напишем то же самое с последовательностями:
fun main() {
//sampleStart
val words = "The quick brown fox jumps over the lazy dog".split(" ")
//convert the List to a Sequence
val wordsSequence = words.asSequence()
val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
.map { println("length: ${it.length}"); it.length }
.take(4)
println("Lengths of first 4 words longer than 3 chars")
// terminal operation: obtaining the result as a List
println(lengthsSequence.toList())
//sampleEnd
}
Вывод этого кода показывает, что функции filter() и map() вызываются только при построении списка результатов. Таким образом, вы сначала видите строку текста "Lengths of..", а затем начинается обработка последовательности. Обратите внимание, что для элементов, оставшихся после фильтрации, map выполняется до фильтрации следующего элемента. Когда размер результата достигает 4, обработка останавливается, потому что это максимальный возможный размер, который может вернуть take(4).
Обработка последовательности происходит так:
В этом примере обработка последовательности занимает 18 шагов вместо 23 шагов при выполнении того же действия со списками.
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/sequences.html