课程名称: 一课掌握Kotlin 突破开发语言瓶颈
课程章节: 案例:Generator 与标准库的序列生成器
课程讲师: bennyhuo
本节是案例讲解课程,案例是通过Kotlin实现一个Python Generator。
fun main() { val nums = generator { start: Int -> for (i in 0..5) { yield(start + i) } } val seq = nums(10) for (j in seq) { println(j) } }
generator { start: Int -> ... }
generator的定义如下:
fun <T> generator(block: suspend GeneratorScope<T>.(T) -> Unit): (T) -> Generator<T> { ... }
suspend GeneratorScope<Int>.(Int) -> Unit
generator { start: Int -> ... }
使用了Lambda表达式的写法,根据上面对generator定义的理解,可以拆分成如下内容去理解:
val job: suspend GeneratorScope<Int>.(Int) -> Unit = {start: Int -> for (i in 0..5) { yield(start + i) } } fun main() { val nums = generator(job) val seq = nums(10) for (j in seq) { println(j) } }
首先,generator使用了Lambda表达式的写法,当Lambda作为函数的最后一个参数传入时,可以写到括号外面。当Lambda写到括号外面后,括号内没有其他参数时,括号可以省略。我们将generator还原成如此:
generator({ start: Int -> for (i in 0..5) { yield(start + i) } })
括号中的内容实际上是一个匿名函数,而函数在Kotlin中是一等公民,它有自己的类型,可以赋值,可以传递,并在合适的条件下调用。
这个匿名函数的类型是suspend GeneratorScope<Int>.(Int) -> Unit
,那么我们就去定义一个属性job,它的类型是suspend GeneratorScope<Int>.(Int) -> Unit
val job: suspend GeneratorScope<Int>.(Int) -> Unit = {start: Int -> for (i in 0..5) { yield(start + i) } }
调用generator的时候,把job传递进去val nums = generator(job)
generator的返回值类型是一个函数类型(T) -> Generator<T>
,该函数类型的返回结果是一个Generator类型。本案例中,generator直接return一个Lambda表达式,Lambda表达式最后一行就是函数表达式的返回值,所以该它返回了一个GeneratorImpl实例。
fun <T> generator(block: suspend GeneratorScope<T>.(T) -> Unit): (T) -> Generator<T> { return { parameter: T -> GeneratorImpl(block, parameter) } }
GeneratorImpl是Generator接口的一个实现类。
interface Generator<T> { operator fun iterator(): Iterator<T> }
从实现效果知道,我们需要Generator可以迭代,所以我们的Generator需要重写iterator这个方法,而iterator在Kotlin中是一个运算符,所以iterator前面加上operator关键字,表明这是一个运算符重载。
GeneratorImpl就是对Generator接口的实现,它实现了iterator方法,在iterator中返回了一个GeneratorIterator实例
class GeneratorImpl<T>(private val block: suspend GeneratorScope<T>.(T) -> Unit, private val parameter: T): Generator<T> { override fun iterator(): Iterator<T> { return GeneratorIterator(block, parameter) } }
GeneratorIterator肯定得实现Iterator接口
override fun hasNext(): Boolean { ... } override fun next(): T { ... }
在初始化seq变量后,对他进行遍历时,按照迭代原理,一开始就会先调用hasNext,判断是否有下一个元素,有的话就调用next,没有的话就结束,后续不断循环hasNext -> next这个流程,直到迭代结束。
为了使自定义的GeneratorIterator能够正常工作,案例定义了一个State密封类
sealed class State { class NotReady(val continuation: Continuation<Unit>): State() class Ready<T>(val continuation: Continuation<Unit>, val nextValue: T): State() object Done: State() }
然后通过State来实现hasNext和next的迭代逻辑
在GeneratorIterator初始化的时候,把状态设置为State.NotReady,开始迭代的时候会先调用hasNext,这时状态时NotReady,我们启动协程。
sealed class State { class NotReady(val continuation: Continuation<Unit>): State() class Ready<T>(val continuation: Continuation<Unit>, val nextValue: T): State() object Done: State() }
然后通过State来实现hasNext和next的迭代逻辑
private fun resume() { when(val currentState = state) { is State.NotReady -> currentState.continuation.resume(Unit) } } override fun hasNext(): Boolean { resume() return state != State.Done }
协程启动后,原来的执行流程会暂停,转去执行协程的方法体
{start: Int -> for (i in 0..5) { yield(start + i) } }
yield也是一个挂起方法,在它内部把状态从NotReady改为Ready
override suspend fun yield(value: T) = suspendCoroutine<Unit> { continuation -> state = when(state) { is State.NotReady -> State.Ready(continuation, value) is State.Ready<*> -> throw IllegalStateException("Cannot yield a value while ready.") State.Done -> throw IllegalStateException("Cannot yield a value while done.") } }
yield执行完后,恢复到主流程,继续执行hasNext,hasNext执行完后就会去调用next。
next中判断到状态为Ready,就会把状态修改为NotReady,并把结果返回
override fun next(): T { return when(val currentState = state) { is State.NotReady -> { resume() return next() } is State.Ready<*> -> { state = State.NotReady(currentState.continuation) (currentState as State.Ready<T>).nextValue } State.Done -> throw IndexOutOfBoundsException("No value left.") } }
至此,一次迭代业务完成。
进入下一次迭代,由于状态时NotReady,会再次启动协程,执行yield把状态改为Ready,然后在next中获取结果,再把状态改为NotReady。
另外,由于foreach在懒序列中不会立即执行,所以for (i in 0…5) {…}会在每次协程启动的时候才迭代一次。
当for (i in 0…5) {…}迭代完了,会调用resumeWith,把状态修改为Done
override fun resumeWith(result: Result<Any?>) { state = State.Done result.getOrThrow() }
状态为Done,hasNext返回false,真个迭代流程就执行完了。
通过对案例的琢磨,除了对新增知识协程有了更深刻的认识外,还对之前章节的知识进行了一遍复习,对Kotlin函数类型,密封类型,Lambda表达式加深了印象。