Spec-Zone.ru › Kotlin 1.9

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

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

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

Sequences processing

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

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

© 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