破解 Kotlin 協程(7) - 序列生成器篇

關鍵詞:Kotlin 協程 序列 Sequencebash

說出來你可能不信,Kotlin 1.1 協程還在吃奶的時候,Sequence 就已經正式推出了,然而,Sequence 生成器的實現竟然有協程的功勞。ide

1. 認識 Sequence

在 Kotlin 當中,Sequence 這個概念確切的說是「懶序列」,產生懶序列的方式能夠有多種,下面咱們介紹一種由基於協程實現的序列生成器。須要注意的是,這個功能內置於 Kotlin 標準庫當中,不須要額外添加依賴。函數

下面咱們給出一個斐波那契數列生成的例子:post

val fibonacci = sequence {
    yield(1L) // first Fibonacci number
    var cur = 1L
    var next = 1L
    while (true) {
        yield(next) // next Fibonacci number
        val tmp = cur + next
        cur = next
        next = tmp
    }
}

fibonacci.take(5).forEach(::log)
複製代碼

這個 sequence 實際上也是啓動了一個協程,yield 則是一個掛起點,每次調用時先將參數保存起來做爲生成的序列迭代器的下一個值,以後返回 COROUTINE_SUSPENDED,這樣協程就再也不繼續執行,而是等待下一次 resume 或者 resumeWithException 的調用,而實際上,這下一次的調用就在生成的序列的迭代器的 next() 調用時執行。如此一來,外部在遍歷序列時,每次須要讀取新值時,協程內部就會執行到下一次 yield 調用。ui

程序運行輸出的結果以下:spa

10:44:34:071 [main] 1
10:44:34:071 [main] 1
10:44:34:071 [main] 2
10:44:34:071 [main] 3
10:44:34:071 [main] 5
複製代碼

除了使用 yield(T) 生成序列的下一個元素之外,咱們還能夠用 yieldAll() 來生成多個元素:.net

val seq = sequence {
    log("yield 1,2,3")
    yieldAll(listOf(1, 2, 3))
    log("yield 4,5,6")
    yieldAll(listOf(4, 5, 6))
    log("yield 7,8,9")
    yieldAll(listOf(7, 8, 9))
}

seq.take(5).forEach(::log)
複製代碼

從運行結果咱們能夠看到,在讀取 4 的時候纔會去執行到 yieldAll(listOf(4, 5, 6)),而因爲 7 之後都沒有被訪問到,yieldAll(listOf(7, 8, 9)) 並不會被執行,這就是所謂的「懶」。code

10:44:34:029 [main] yield 1,2,3
10:44:34:060 [main] 1
10:44:34:060 [main] 2
10:44:34:060 [main] 3
10:44:34:061 [main] yield 4,5,6
10:44:34:061 [main] 4
10:44:34:066 [main] 5
複製代碼

2. 深刻序列生成器

前面咱們已經不止一次提到 COROUTINE_SUSPENDED 了,咱們也很容易就知道 yieldyieldAll 都是 suspend 函數,既然能作到」懶「,那麼必然在 yieldyieldAll 處是掛起的,所以它們的返回值必定是 COROUTINE_SUSPENDED,這一點咱們在本文的開頭就已經提到,下面咱們來見識一下廬山真面目:協程

override suspend fun yield(value: T) {
    nextValue = value
    state = State_Ready
    return suspendCoroutineUninterceptedOrReturn { c ->
        nextStep = c
        COROUTINE_SUSPENDED
    }
}
複製代碼

這是 yield 的實現,咱們看到了老朋友 suspendCoroutineUninterceptedOrReturn,還看到了 COROUTINE_SUSPENDED,那麼掛起的問題就很好理解了。而 yieldAll 是一模一樣:blog

override suspend fun yieldAll(iterator: Iterator<T>) {
    if (!iterator.hasNext()) return
    nextIterator = iterator
    state = State_ManyReady
    return suspendCoroutineUninterceptedOrReturn { c ->
        nextStep = c
        COROUTINE_SUSPENDED
    }
}
複製代碼

惟一的不一樣在於 state 的值,一個流轉到了 State_Ready,一個是 State_ManyReady,也卻是很好理解嘛。

那麼如今就剩下一個問題了,既然有了掛起,那麼何時執行 resume ?這個很容易想到,咱們在迭代序列的時候唄,也就是序列迭代器的 next() 的時候,那麼這事兒就好辦了,找下序列的迭代器實現便可,這個類型咱們也很容易找到,顯然 yield 就是它的方法,咱們來看看 next 方法的實現:

override fun next(): T {
    when (state) {
        State_NotReady, State_ManyNotReady -> return nextNotReady() // ①
        State_ManyReady -> { // ②
            state = State_ManyNotReady
            return nextIterator!!.next()
        }
        State_Ready -> { // ③
            state = State_NotReady
            val result = nextValue as T
            nextValue = null
            return result
        }
        else -> throw exceptionalState()
    }
}
複製代碼

咱們來依次看下這三個條件:

  • ① 是下一個元素尚未準備好的狀況,調用 nextNotReady 會首先調用 hasNext 檢查是否有下一個元素,檢查的過程其實就是調用 Continuation.resume,若是有元素,就會再次調用 next,不然就拋異常
  • ② 表示咱們調用了 yieldAll,一會兒傳入了不少元素,目前尚未讀取完,所以須要繼續從傳入的這個元素集合當中去迭代
  • ③ 表示咱們調用了一次 yield,而這個元素的值就存在 nextValue 當中

hasNext 的實現也不是很複雜:

override fun hasNext(): Boolean {
    while (true) {
        when (state) {
            State_NotReady -> {} // ①
            State_ManyNotReady -> // ②
                if (nextIterator!!.hasNext()) {
                    state = State_ManyReady
                    return true
                } else {
                    nextIterator = null
                }
            State_Done -> return false // ③
            State_Ready, State_ManyReady -> return true // ④
            else -> throw exceptionalState()
        }

        state = State_Failed
        val step = nextStep!!
        nextStep = null
        step.resume(Unit)
    }
}
複製代碼

咱們在經過 next 讀取完一個元素以後,若是已經傳入的元素已經沒有剩餘,狀態會轉爲 State_NotReady,下一次取元素的時候就會在 next 中觸發到 hasNext 的調用,① 處什麼都沒有幹,所以會直接落到後面的 step.resume(),這樣就會繼續執行咱們序列生成器的代碼,直到遇到 yield 或者 yieldAll

3. 小結

序列生成器很好的利用了協程的狀態機特性,將序列生成的過程從形式上整合到了一塊兒,讓程序更加緊湊,表現力更強。本節討論的序列,某種意義上更像是生產 - 消費者模型中的生產者,而迭代序列的一方則像是消費者,其實在 kotlinx.coroutines 庫中提供了更爲強大的能力來實現生產 - 消費者模式,咱們將在後面的文章當中展現給你們看。

協程的回調特性可讓咱們在實踐當中很好的替代傳統回調的寫法,同時它的狀態機特性也可讓曾經的狀態機實現得到新的寫法,除了序列以外,也許還會有更多有趣的適用場景等待咱們去發掘~


歡迎關注 Kotlin 中文社區!

中文官網:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公衆號:Kotlin

知乎專欄:Kotlin

CSDN:Kotlin中文社區

掘金:Kotlin中文社區

簡書:Kotlin中文社區

相關文章
相關標籤/搜索