Представление нескольких значений в Kotlin: список, последовательность и поток

При работе с несколькими значениями в Kotlin доступно несколько вариантов. В этом посте мы рассмотрим список, последовательность и поток. Чтобы лучше понять эти 3 варианта и их различия, мы определим гипотетическую проблему и попытаемся уточнить наше решение, пока не достигнем желаемого результата.

Проблема

Мы хотим написать функцию, которая возвращает первые 10 чисел Фибоначчи. Мы предполагаем, что вычисление каждого числа занимает 500 миллисекунд. Также предположим, что идеальное решение, которое мы ищем, должно обладать следующими свойствами:

  1. Решение должно быть неблокирующим, что означает, что оно ни в коем случае не должно блокировать основной поток.
  2. Каждое число должно быть доступно, как только оно вычислено.

Список

Мы начнем с определения следующей функции, которая возвращает первые 10 чисел Фибоначчи в виде списка.

fun fibonacciList(): List<Int> {
    var first = 0
    var second = 1
    val numbersList = mutableListOf<Int>().apply {
        Thread.sleep(500)
        add(first)
        Thread.sleep(500)
        add(second)
    }
    repeat(8) {
        Thread.sleep(500)
        val t = second
        second += first
        first = t
        numbersList += second
    }
    return numbersList
}

В приведенном выше коде мы использовали Thread.sleep для имитации времени, необходимого для вычисления числа.

Давайте вызовем вышеуказанную функцию следующим образом и посмотрим, что будет напечатано:

fun main() {
    lateinit var numbers: List<Int>
    val duration = measureTimeMillis {
        numbers = fibonacciList()
    }
    println("fibonacciList returned after $duration millis.")
    numbers.forEach { print("$it ") }
}

Приведенный выше код выводит что-то вроде следующего:

fibonacciList returned after 5034 millis.
0 1 1 2 3 5 8 13 21 34

На основе приведенного выше вывода мы можем наблюдать две вещи:

  1. Вызов fibonacciList блокирует основной поток до тех пор, пока он не вернется,
  2. При звонке fibonacciList у нас нет номеров примерно 5 секунд, а после этого у нас будут все номера сразу.

Таким образом, использование списка в качестве возвращаемого типа функции было плохой идеей, поскольку оно не удовлетворяло свойствам решения, которое мы искали. Далее попробуем Sequence.

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

На этот раз мы будем использовать Sequence в качестве типа возвращаемого значения функции, что приведет к следующему коду:

fun fibonacciSequence(): Sequence<Int> {
    var first = 0
    var second = 1
    val numbersSequence = sequence {
        Thread.sleep(500)
        yield(first)
        Thread.sleep(500)
        yield(second)
        repeat(8) {
            Thread.sleep(500)
            val t = second
            second += first
            first = t
            yield(second)
        }
    }
    return numbersSequence
}

Мы вызываем вышеуказанную функцию следующим образом:

fun main() {
    lateinit var numbers: Sequence<Int>
    val duration1 = measureTimeMillis {
        numbers = fibonacciSequence()
    }
    println("fibonacciSequence returned after $duration1 millis.")
    val duration2 = measureTimeMillis {
        numbers.forEach { print("$it ") }
    }
    println("\nIt took $duration2 millis to print all numbers.")
}

Запуск приведенного выше кода генерирует такой вывод:

fibonacciSequence returned after 8 millis.
0 1 1 2 3 5 8 13 21 34 
It took 5040 millis to print all numbers.

Когда мы запускаем приведенный выше код, мы видим, что fibonacciSequence не блокирует и быстро возвращается. Но вызов forEach в последовательности блокируется, и для печати всех чисел требуется примерно 5 секунд. Хорошей новостью является то, что каждый номер печатается, как только он становится доступным. Итак, мы добились определенного прогресса! Давайте перейдем к нашему следующему варианту — Flow.

Поток

Давайте напишем ту же функцию, но на этот раз с типом возвращаемого значения Flow:

fun fibonacciFlow(): Flow<Int> {
    var first = 0
    var second = 1
    val numbersFlow = flow {
        delay(500)
        emit(first)
        delay(500)
        emit(second)
        repeat(8) {
            delay(500)
            val t = second
            second += first
            first = t
            emit(second)
        }
    }
    return numbersFlow
}

Давайте вызовем вышеуказанную функцию следующим образом:

fun main() = runBlocking {
    lateinit var numbers: Flow<Int>
    val duration = measureTimeMillis {
        numbers = fibonacciFlow()
    }
    println("fibonacciFlow returned after $duration millis.")
    launch {
        numbers.collect { print("$it ") }
    }
    println("Main thread is not blocked!")
}

Выполнение приведенного выше кода приводит к следующему результату:

fibonacciFlow returned after 6 millis.
Main thread is not blocked!
0 1 1 2 3 5 8 13 21 34

Запустив приведенный выше код, мы можем наблюдать следующие вещи:

  1. Весь код неблокирующий,
  2. Каждое число печатается сразу после его создания.

Это значит, что мы нашли идеальное решение! У нас есть функция, которая генерирует первые 10 чисел Фибоначчи. Он не блокирует основной поток, и каждое число становится доступным сразу после его создания.

И это завершение!