Spec-Zone.ru › Kotlin 1.6

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

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

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

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..”, а затем начинается обработка последовательности. Обратите внимание, что для элементов, оставшихся после фильтрации, карта выполняется до фильтрации следующего элемента. Когда размер результата достигает 4, обработка останавливается, так как это максимальный возможный размер, который может вернуть take(4).

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

Sequences processing

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

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

© 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

Spec-Zone.ru

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