--- title: Sequence description: Ленивые вычисления, порядок обхода элементов и когда Sequence выигрывает у List tags: - kotlin - collections --- `Sequence` - ленивый аналог коллекции. Операции не выполняются до запроса результата, и элементы проходят всю цепочку по одному. ```kotlin fun main() { val list = listOf(1, 2, 3, 4, 5) println("-- List:") val eager = list .map { println(" map $it"); it * 2 } .filter { println(" filter $it"); it > 4 } .first() println(" результат $eager") println("-- Sequence:") val lazy = list.asSequence() .map { println(" map $it"); it * 2 } .filter { println(" filter $it"); it > 4 } .first() println(" результат $lazy") } ``` `List` выполнит 5 `map` и 5 `filter` - всего 10 операций, создав два промежуточных списка. `Sequence` выполнит 3 `map` и 3 `filter` и остановится, как только найдёт первый подходящий элемент. ## Два порядка обхода Это ключ к пониманию `Sequence`: ```mermaid graph LR subgraph List["List: по операциям"] L1["map всех 5"] --> L2["filter всех 5"] --> L3["first"] end subgraph Seq["Sequence: по элементам"] S1["1: map, filter"] --> S2["2: map, filter"] --> S3["3: map, filter → стоп"] end ``` | | `List` | `Sequence` | |---|---|---| | порядок | операция за операцией | элемент за элементом | | промежуточные коллекции | на каждом шаге | нет | | вычисление | сразу | при терминальной операции | | можно ли прервать | нет | да | | бесконечные данные | нет | да | ## Создание ```kotlin fun main() { println(listOf(1, 2, 3).asSequence().toList()) // [1, 2, 3] println(sequenceOf(1, 2, 3).toList()) // [1, 2, 3] println(emptySequence().toList()) // [] println(generateSequence(1) { it * 2 }.take(5).toList()) // [1, 2, 4, 8, 16] println(generateSequence(1) { if (it < 5) it + 1 else null }.toList()) // [1, 2, 3, 4, 5] val built = sequence { yield(1) yieldAll(listOf(2, 3)) yield(4) } println(built.toList()) // [1, 2, 3, 4] } ``` `generateSequence` с `null` в лямбде - способ задать конец. `sequence { }` - билдер с `yield`, внутри него можно писать любую логику, включая циклы. ## Промежуточные и терминальные операции ```kotlin fun main() { val seq = sequenceOf(1, 2, 3) // промежуточные: возвращают Sequence, ничего не считают val chain = seq.map { it * 2 }.filter { it > 2 }.take(2) println("цепочка построена, вычислений не было") // терминальные: запускают вычисление println(chain.toList()) // [4, 6] println(seq.sum()) // 6 println(seq.count()) // 3 println(seq.first()) // 1 println(seq.any { it > 2 }) // true } ``` | Промежуточные | Терминальные | |---|---| | `map`, `filter`, `flatMap` | `toList`, `toSet`, `toMap` | | `take`, `drop`, `distinct` | `first`, `last`, `single` | | `sorted`, `chunked`, `windowed` | `sum`, `count`, `average`, `max` | | `onEach`, `mapNotNull` | `any`, `all`, `none`, `forEach` | | `zip`, `plus` | `fold`, `reduce`, `joinToString` | ## Бесконечные последовательности То, чего `List` не умеет в принципе: ```kotlin fun main() { val naturals = generateSequence(1) { it + 1 } println(naturals.take(5).toList()) // [1, 2, 3, 4, 5] println(naturals.filter { it % 7 == 0 }.first()) // 7 println(naturals.map { it * it }.takeWhile { it < 50 }.toList()) // [1, 4, 9, 16, 25, 36, 49] val fib = generateSequence(0 to 1) { (a, b) -> b to (a + b) }.map { it.first } println(fib.take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] } ``` Важно: терминальная операция без ограничения на бесконечной последовательности зависнет. `naturals.toList()` не завершится никогда. ## Sequence в цепочке с ранним выходом ```kotlin data class Record3(val id: Int, val active: Boolean, val score: Int) fun main() { val records = List(1_000_000) { Record3(it, it % 100 == 0, it % 7) } val found = records.asSequence() .filter { it.active } .map { it.score * 2 } .firstOrNull { it > 10 } println(found) // 12 val top3 = records.asSequence() .filter { it.active } .take(3) .map { it.id } .toList() println(top3) // [0, 100, 200] } ``` Здесь `Sequence` обрабатывает несколько сотен элементов вместо миллиона. На `List` каждый `filter`/`map` прошёл бы весь миллион и создал два списка. ## Когда Sequence не выигрывает ```kotlin fun main() { val small = listOf(1, 2, 3, 4, 5) // Sequence здесь только добавляет накладные расходы println(small.map { it * 2 }.sum()) // 30 println(small.asSequence().map { it * 2 }.sum()) // 30, но медленнее } ``` `Sequence` создаёт объект-обёртку на каждую операцию и вызывает лямбды через интерфейс итератора. На маленьких коллекциях и коротких цепочках это дороже, чем прямой проход. Практическое правило: | Условие | Выбор | |---|---| | меньше ~1000 элементов, 1-2 операции | `List` | | много элементов и 3+ операций | `Sequence` | | есть `first`, `find`, `take`, `any` после цепочки | `Sequence` | | источник бесконечный или потоковый (файл, БД) | `Sequence` | | нужен результат целиком, операций мало | `List` | | нужен индексный доступ к результату | `List` | ## Операции, которые ломают ленивость Некоторые операции обязаны прочитать всё, прежде чем выдать первый элемент: ```kotlin fun main() { val seq = sequenceOf(3, 1, 2) .onEach { println("читаю $it") } .sorted() // нужны все элементы println("до first ничего не прочитано") println(seq.first()) // здесь прочитаются все три } ``` Такие операции - `sorted`, `sortedBy`, `distinct` (частично), `groupBy`, `chunked`. Ставьте их как можно позже в цепочке, а `filter` - как можно раньше. ## Sequence одноразов ```kotlin fun main() { val seq = listOf(1, 2, 3).asSequence().map { it * 2 } println(seq.toList()) // [2, 4, 6] println(seq.toList()) // [2, 4, 6] — здесь повезло: источник List val once = sequence { yield(1); yield(2) } println(once.toList()) // [1, 2] println(once.toList()) // [1, 2] — тоже работает, лямбда вызвана заново val fromIterator = listOf(1, 2, 3).iterator().asSequence() println(fromIterator.toList()) // [1, 2, 3] try { fromIterator.toList() } catch (e: IllegalStateException) { println(e.message) // This sequence can be consumed only once. } } ``` Последовательность из итератора обходится один раз: `Iterator.asSequence()` оборачивает результат в `constrainOnce()`, который фиксирует это ограничение и бросает `IllegalStateException` при повторной попытке. Тот же механизм доступен явно: ```kotlin fun main() { val seq = sequenceOf(1, 2).constrainOnce() println(seq.toList()) // [1, 2] try { seq.toList() } catch (e: IllegalStateException) { println("повторный обход запрещён") } } ``` ## Чтение файла Главный практический случай: данные, которые не влезают в память. ```kotlin import java.io.File fun main() { val tmp = File.createTempFile("demo", ".txt") tmp.writeText((1..100).joinToString("\n")) val sum = tmp.useLines { lines -> // Sequence lines.map { it.toInt() }.filter { it % 2 == 0 }.sum() } println(sum) // 2550 val firstBig = tmp.useLines { lines -> lines.first { it.toInt() > 50 } } println(firstBig) // 51 — файл прочитан до 51-й строки, не целиком tmp.delete() } ``` `useLines` даёт `Sequence` и закрывает файл после работы. Альтернатива `readLines()` загрузила бы весь файл в `List`. ## Sequence и Flow `Sequence` - ленивый, но **синхронный**: получение следующего элемента блокирует поток. Для асинхронных источников нужен `Flow`: | | `Sequence` | `Flow` | |---|---|---| | вычисление | лениво | лениво | | блокировка | да | нет, `suspend` | | внутри можно `delay` | нет | да | | источник | память, файл, генератор | сеть, БД, события | Подробнее: [Flow](../Корутины/Flow.md) > [!info] Отличия от Java > | Java Stream | Kotlin Sequence | > |---|---| > | `list.stream()` | `list.asSequence()` | > | `Stream.of(1, 2)` | `sequenceOf(1, 2)` | > | `Stream.iterate(1, f)` | `generateSequence(1, f)` | > | `collect(toList())` | `toList()` | > | `.parallel()` | **нет аналога** | > | `IntStream`, `LongStream` | нет, только `Sequence` | > | `Files.lines(path)` | `file.useLines { }` | > | нет генератора с `yield` | `sequence { yield(x) }` | > > **`Sequence` и `Stream` - почти одно и то же.** Ленивость, промежуточные и терминальные операции, короткое замыкание на `findFirst`/`first` - всё совпадает. Поэтому опыт работы со стримами переносится напрямую, и наоборот: если в Java вы написали бы `.stream()`, в Kotlin нужен `.asSequence()`. > > **Главное отличие - нет параллелизма.** `Stream.parallel()` распределяет работу по `ForkJoinPool`; у `Sequence` такого нет вообще. Причина в том, что для конкурентности в Kotlin предусмотрены корутины, а не параллельные стримы. Если нужна параллельная обработка - либо используйте Java-стримы напрямую (они доступны), либо корутины: > ```kotlin > fun main() { > // Java Stream доступен из Kotlin > val sum = listOf(1, 2, 3).stream().parallel().mapToInt { it * 2 }.sum() > println(sum) // 12 > } > ``` > > **Нет специализаций для примитивов.** В Java `IntStream` избегает упаковки: `list.stream().mapToInt(x -> x).sum()` работает с `int`. `Sequence` упаковывает каждый элемент в `Integer`. На числовых расчётах по большим данным Java-стримы могут оказаться быстрее. > > **`sequence { yield(x) }` - генератор, которого в Java нет.** Он реализован на том же механизме приостановки, что и корутины, поэтому внутри можно писать обычные циклы и условия: > ```kotlin > fun main() { > val primes = sequence { > var n = 2 > while (true) { > if ((2 until n).none { n % it == 0 }) yield(n) > n++ > } > } > println(primes.take(5).toList()) // [2, 3, 5, 7, 11] > } > ``` > В Java это требует либо отдельного потока, либо ручной реализации `Iterator` с сохранением состояния. > > Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md) > [!warning] Подводные камни > **Без терминальной операции не произойдёт ничего:** > ```kotlin > fun main() { > listOf(1, 2, 3).asSequence().map { println("это не выведется"); it } > println("конец") // только "конец" > } > ``` > Это частая ошибка при замене `forEach` на цепочку: если забыть терминальную операцию, код просто не выполнится, и компилятор не предупредит. > > **`Sequence` на маленьких коллекциях медленнее.** Обёртки и вызовы через итератор стоят дороже прямого цикла. Для списка из 10 элементов `asSequence()` - пессимизация. > > **Терминальная операция на бесконечной последовательности зависает:** > ```kotlin > fun main() { > val naturals = generateSequence(1) { it + 1 } > println(naturals.take(3).toList()) // [1, 2, 3] — ок > // naturals.toList() // зависнет навсегда > // naturals.count() // тоже > // naturals.filter { it > 10 }.toList() // и это > } > ``` > Любая цепочка из бесконечного источника обязана содержать `take`, `takeWhile`, `first` или `find`. > > **`sorted` и `groupBy` убивают ленивость** - они читают всё до первого результата. Цепочка `asSequence().sorted().first()` не быстрее, чем на `List`. > > **Последовательность из итератора обходится один раз** - второй вызов падает: > ```kotlin > fun main() { > val seq = listOf(1, 2).iterator().asSequence() > println(seq.count()) // 2 > try { > seq.count() > } catch (e: IllegalStateException) { > println("повторный обход: ${e.message}") > } > } > ``` > Исключение здесь лучше молчаливого пустого результата, но возникает оно в рантайме и только на втором обходе - в редкой ветке кода его легко не заметить. Если последовательность может понадобиться дважды, сохраните `toList()`. > > **Побочные эффекты в промежуточных операциях выполняются в неожиданном порядке:** > ```kotlin > fun main() { > val log = mutableListOf() > sequenceOf(1, 2) > .onEach { log.add("A$it") } > .onEach { log.add("B$it") } > .toList() > println(log) // [A1, B1, A2, B2] — не [A1, A2, B1, B2] > } > ``` > На `List` вывод был бы `[A1, A2, B1, B2]`. Код, зависящий от порядка побочных эффектов, при переходе на `Sequence` сломается. > > **Повторный обход пересчитывает всё заново:** > ```kotlin > fun main() { > var calls = 0 > val seq = listOf(1, 2, 3).asSequence().map { calls++; it * 2 } > seq.toList() > seq.toList() > println(calls) // 6 — лямбда вызвана дважды для каждого элемента > } > ``` > Если результат нужен несколько раз, сохраните его в `List`. > > **`Sequence` синхронна.** Внутри её лямбд нельзя вызывать `suspend`-функции - для этого существует `Flow`. Попытка использовать `Sequence` для сетевых запросов приведёт к блокировке потока. > > **`useLines` закрывает файл по выходу из лямбды.** Вернуть `Sequence` наружу нельзя - она окажется на закрытом ресурсе: > ```kotlin > import java.io.File > > fun main() { > val tmp = File.createTempFile("leak", ".txt") > tmp.writeText("1\n2") > val leaked = tmp.useLines { it } // Sequence на закрытом файле > try { > println(leaked.toList()) > } catch (e: java.io.IOException) { > println("поток закрыт: ${e.message}") // поток закрыт: Stream closed > } > tmp.delete() > } > ``` > Внутри лямбды нужно получить готовое значение: `useLines { it.toList() }` или сразу агрегат. Далее: [Коллекции под капотом](Коллекции%20под%20капотом.md)