Spec-Zone.ru › Kotlin 1.8

Последовательности

Наряду со списками в стандартной библиотеке 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 на примере.

Список

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

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: для элементов, оставшихся после фильтрации, а затем вывод двух последних строк.

Вот как происходит обработка списка:

List processing

Последовательность

Теперь напишем то же самое с последовательностями:

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).

Обработка последовательности происходит следующим образом:

Sequences processing

В этом примере обработка последовательности занимает 18 шагов вместо 23 шагов для обработки списков.

Последнее изменение: 10 января 2023 г.
Диапазоны и прогрессии Обзор операций со списками

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/sequences.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API