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

关键词:Kotlin 协程 序列 Sequence

说出来你可能不信,Kotlin 1.1 协程还在吃奶的时候,Sequence 就已经正式推出了,然而,Sequence 生成器的实现居然有协程的功劳。

1. 认识 Sequence

在 Kotlin 当中,Sequence 这个概念确切的说是“懒序列”,产生懒序列的方式可以有多种,下面我们介绍一种由基于协程实现的序列生成器。需要注意的是,这个功能内置于 Kotlin 标准库当中,不需要额外添加依赖。

下面我们给出一个斐波那契数列生成的例子:

 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 调用。

程序运行输出的结果如下:

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() 来生成多个元素:

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)) 并不会被执行,这就是所谓的“懒”。

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 是如出一辙:

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 中文社区!

中文官网:https://www.kotlincn.net/

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

公众号:Kotlin

知乎专栏:Kotlin

CSDN:Kotlin中文社区

掘金:Kotlin中文社区

简书:Kotlin中文社区

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容