【九月打卡】第2天 一课掌握Kotlin 突破开发语言瓶颈

2022/9/8 3:23:10

本文主要是介绍【九月打卡】第2天 一课掌握Kotlin 突破开发语言瓶颈,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

课程信息

课程名称: 一课掌握Kotlin 突破开发语言瓶颈
课程章节: 案例:Generator 与标准库的序列生成器
课程讲师: bennyhuo

课程内容

本节是案例讲解课程,案例是通过Kotlin实现一个Python Generator。

案例目标

图片描述

Python Generator样例

图片描述

Kotlin实现效果

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> {
	...
}
  • generator是一个泛型函数
  • 它的接收一个挂起函数作为参数,挂起函数的类型为suspend GeneratorScope<Int>.(Int) -> Unit
  • 它的返回值类型是Generator

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)
    }
}

Generator解析

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表达式加深了印象。



这篇关于【九月打卡】第2天 一课掌握Kotlin 突破开发语言瓶颈的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程