5、协程与高阶函数:suspend函数、async/await、Flow的创建与操作、高阶函数在协程中的应用、协程上下文与调度器

协程这东西,刚接触时觉得玄乎,用久了你会发现它其实就是一套「能把异步代码写得像同步一样」的框架。我个人习惯把协程理解为「轻量级线程」——它不绑定操作系统线程,可以在线程间随意跳转,挂起和恢复的成本极低。

今天我们就来聊聊协程和高阶函数怎么配合,以及那些你天天用但可能没深想过的概念。

suspend函数:协程的基本单元

suspend 函数是协程的基石。说白了,就是一个可以被挂起、稍后再恢复的函数。挂起时不会阻塞线程,这点和回调不一样。

suspend fun fetchUserData(): User {
    // 模拟网络请求
    delay(1000)
    return User("张三")
}

我在项目中遇到过一个问题:新手容易在 suspend 函数里直接调用另一个 suspend 函数,却忘了它只能在协程作用域内执行。嗯,这里要注意——suspend 函数必须在协程里才能跑,否则编译都过不去。

核心要点:suspend 函数可以调用其他 suspend 函数,但不能在普通函数里直接调用 suspend 函数。你需要用 GlobalScope.launch、runBlocking 或者 lifecycleScope 来启动协程。

async/await:并行任务的利器

async 和 await 是 Kotlin 协程里我最喜欢的组合。async 启动一个协程并返回 Deferred 对象,await 等待结果。两个任务可以并行执行,而不是串行等待。

coroutineScope {
    val user = async { fetchUser() }
    val posts = async { fetchPosts() }
    
    // 两个请求并行,都完成后才继续
    val result = Pair(user.await(), posts.await())
}

我曾经在做一个首页聚合接口时,需要同时拉取用户信息、推荐列表和广告数据。如果用回调嵌套,代码会变成「回调地狱」。用 async/await 后,三个请求并行,代码却像同步一样清晰。

小技巧:如果你有多个不依赖彼此的网络请求,一定要用 async 并行执行。串行的话,总耗时是各请求耗时之和;并行的话,总耗时是最慢的那个请求的耗时。

Flow:响应式数据流

Flow 是 Kotlin 协程里的响应式数据流,类似于 RxJava 的 Observable,但更轻量、更自然。它适合处理那些会多次发射数据的情景,比如数据库监听、传感器数据、下载进度等。

创建 Flow

fun countDown(): Flow<Int> = flow {
    for (i in 10 downTo 0) {
        emit(i)
        delay(1000)
    }
}

操作 Flow

Flow 的操作符和高阶函数密不可分。map、filter、catch 这些本质上都是高阶函数——它们接收 lambda 作为参数,对数据流进行变换。

countDown()
    .filter { it % 2 == 0 }  // 只保留偶数
    .map { "剩余 $it 秒" }
    .catch { e -> emit("出错了: ${e.message}") }
    .collect { println(it) }

你想想看,如果没有高阶函数,这些操作得写多少样板代码?Flow 的设计哲学就是「用高阶函数组合数据流」,这和函数式编程的思路一脉相承。

避坑指南:我曾经在 collect 里直接做了耗时操作,导致 UI 卡顿。记住,collect 默认运行在调用它的协程上下文中。如果你在 Android 主线程 collect,里面就别做耗时操作。用 flowOn 切换调度器可以解决这个问题。

高阶函数在协程中的应用

协程和高阶函数的结合,远不止 Flow 操作符这么简单。我们来看几个典型场景。

自定义协程构建器

你可以用高阶函数封装协程的启动逻辑。比如封装一个带超时控制的网络请求:

suspend fun <T> withTimeoutRequest(
    timeout: Long,
    block: suspend () -> T
): T {
    return withTimeout(timeout) {
        block()
    }
}

// 使用
val result = withTimeoutRequest(5000) {
    fetchData()
}

这里 block 就是一个 suspend lambda,是高阶函数的典型用法。我经常用这种方式封装一些通用逻辑,比如重试、缓存、超时等。

协程作用域函数

coroutineScope、withContext、supervisorScope 这些函数,本质上都是高阶函数。它们接收一个 suspend lambda,并在特定的上下文中执行。

suspend fun loadData(): Data = coroutineScope {
    val part1 = async { loadPart1() }
    val part2 = async { loadPart2() }
    Data(part1.await(), part2.await())
}

coroutineScope 会等待所有子协程完成,并且如果任何一个子协程抛出异常,整个作用域都会取消。这就是结构化并发的核心。

协程上下文与调度器

协程上下文是协程运行时的环境信息集合。它包含调度器、异常处理器、协程名称等元素。调度器决定了协程在哪个线程上执行。

调度器 用途 示例场景
Dispatchers.Main 主线程,用于 UI 操作 更新 TextView、显示 Dialog
Dispatchers.IO IO 密集型任务 网络请求、数据库读写、文件操作
Dispatchers.Default CPU 密集型任务 图片压缩、数据解析、复杂计算
Dispatchers.Unconfined 不限制线程(不推荐日常使用) 测试或特殊场景

我在项目中经常用 withContext 切换调度器。比如从数据库读取数据后,切回主线程更新 UI:

suspend fun loadAndShow() {
    val data = withContext(Dispatchers.IO) {
        database.loadData()
    }
    // 自动回到 Dispatchers.Main
    textView.text = data.toString()
}

为什么会这样?因为 withContext 会继承外部的协程上下文,但用指定的调度器替换原来的。如果你在 lifecycleScope.launch(Dispatchers.Main) 里调用 withContext(Dispatchers.IO),执行完 IO 任务后会自动回到 Main。

记住一条原则:不要在主线程做耗时操作,也不要在 IO 线程更新 UI。用 withContext 或 flowOn 明确指定调度器,代码的可读性和安全性都会提升。

知识体系总览

下面这张图总结了协程与高阶函数的核心关系:

协程与高阶函数 suspend 函数 async / await Flow 数据流 上下文与调度器 挂起与恢复 非阻塞等待 并行任务 Deferred 结果 emit / collect 操作符组合 线程切换 结构化并发 核心思想:用高阶函数组合协程,实现优雅的异步编程

这张图展示了协程的四个核心概念以及它们之间的关系。suspend 函数是基础,async/await 解决并行问题,Flow 处理数据流,上下文和调度器控制执行环境。而高阶函数就像胶水一样,把这些概念粘合在一起。

我个人觉得,协程最难的不是语法,而是思维方式的转变。从回调思维切换到协程思维,需要一点时间。但一旦你习惯了,就会觉得「回不去了」——协程配合高阶函数,写出来的代码既简洁又健壮。

最后分享一个经验:刚开始用协程时,别想着一步到位。先从 launch 和 delay 开始,慢慢过渡到 async/await,再尝试 Flow。每一步都亲手写几个例子,比看十遍文档都管用。


公众号:蓝海资料掘金营,微信deep3321