Kotlin 协程实战:实现异步值加载委托,对值进行异步懒初始化

在实际开发中,我们经常遇到这样的场景。

  • 需要立即初始化但计算成本高昂的值
kotlin 复制代码
val expensiveValue = calculateExpensiveValue() 
  • 可能引发阻塞的初始化操作
kotlin 复制代码
val resource = loadResourceFromNetwork()

这些场景通常需要满足以下需求:

  1. 异步加载:避免阻塞主线流程
  2. 惰性计算:按需获取结果
  3. 结果缓存:多次访问时直接返回
  4. 状态同步:支持阻塞等待和异步等待
  5. 动态更新:允许主动设置最终值

异步计算值

通过 Kotlin 协程的 async/await 模型实现异步计算能力。AsyncValue 在初始化时立即通过 CoroutineScope.async 启动后台计算任务,并通过 Deferred.await() 实现挂起等待:

kotlin 复制代码
var finished = false
var result: T? = null
var exception: Throwable? = null
private var production: Deferred<T?> = CoroutineScope(dispatcher).async {
    runCatching { producer() }
        .onFailure { exception = it }
        .onSuccess { result = result ?: it }
        .getOrNull()
}

关键实现细节:

  1. 协程调度 :使用 Dispatchers.IO 作为默认调度器,适用于 IO 密集型任务
  2. 原子操作 :通过 runCatching 统一处理异常,确保结果状态一致性
  3. 结果缓存 :计算完成后将结果存储在 result 字段中,后续访问直接返回
  4. 异常传递 :异常信息会保留在 exception 字段中,等待消费时抛出

委托方式返回和设置值

通过实现 getValue/setValue 操作符函数,提供属性委托访问能力:

kotlin 复制代码
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
    val result = runBlocking { await() }
    if (result == null && exception != null) throw exception!!
    if (result == null) throw NullPointerException("The value[$property] produced is null")
    return result
}

/**
* 获取值,如果值还未生产完成,则挂起协程等待
*/
suspend fun await(): T? = result ?: production.await().apply {
   if (exception != null) throw exception!!
}

特性说明:

  1. 阻塞等待 :在普通上下文访问时,通过 runBlocking 阻塞当前线程等待结果
  2. 异常传播:保留原始异常堆栈信息,便于调试定位
  3. 空值防护:禁止返回 null 值,强制显式处理空值情况

动态更新能力:

kotlin 复制代码
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    runCatching { production.cancel() }
    result = value
}

更新流程:

  1. 取消正在进行的异步计算
  2. 直接设置最终值
  3. 后续访问将返回新值

使用示例:

kotlin 复制代码
class ImageLoader {
    var currentImage: BufferedImage by AsyncValue {
        ImageIO.read(File("big_image.png")) // 耗时加载
    }

    // 动态切换图片
    fun changeImage(newImage: BufferedImage) {
        currentImage = newImage
    }
	
	// 允许产生空值
	val optional: Optional<String> by AsyncValue {
	    Optional.ofNullable(null)
	}
}

完整代码

kotlin 复制代码
class AsyncValue<T>(
    dispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val producer: suspend () -> T,
) {
    /**
     * 非委托下可以访问
     * 是否已经生产完成
     */
    var finished = false
        private set

    /**
     * 非委托下可以访问
     * 产生的值,如果值还未生产完成、异常报错、产生Null,则返回null
     */
    var result: T? = null
        private set

    /**
     * 非委托下可以访问
     * 产生的异常,如果异常未产生、未异常报错则返回null
     */
    var exception: Throwable? = null
        private set

    private var production: Deferred<T?> = CoroutineScope(dispatcher).async {
        runCatching { producer() }
            .onFailure { exception = it }
            .onSuccess { result = result ?: it }
            .getOrNull()
            .apply { finished = true }
    }

    /**
     * 获取值,如果值还未生产完成,则挂起协程等待
     */
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        val result = runBlocking { await() }
        if (result == null && exception != null) throw exception!!
        if (result == null) throw NullPointerException("The value[$property] produced is null")
        return result
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        runCatching { production.cancel() }
        result = value
    }

    /**
     * 获取值,如果值还未生产完成,则挂起协程等待
     */
    suspend fun await(): T? = result ?: production.await().apply {
        if (exception != null) throw exception!!
    }
}

典型应用场景

资源预加载

kotlin 复制代码
val font by AsyncValue {
    // 耗时字体加载
    Font.createFont(Font.TRUETYPE_FONT, File("big_font.ttf"))
}

网络请求聚合

kotlin 复制代码
class WeatherService {
    var weatherData by AsyncValue {
        apiService.fetchWeather() // 网络请求
    }

    fun showWeather() {
        // 页面显示时自动触发异步加载
        val data = weatherData 
        updateUI(data)
    }
}

计算缓存优化

kotlin 复制代码
object DataProcessor {
    private val processedData by AsyncValue {
        heavyComputation().also { 
            saveToCache(it) // 自动缓存计算结果
        }
    }

    fun getData() = processedData
}
相关推荐
猿究院_夏燊6 小时前
Spring Bean 生命周期
java·后端·spring
Wang's Blog7 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
信誓旦旦的程序猿7 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河7 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
Jlzn88887 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
香吧香8 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
我是人✓8 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
原则猫8 小时前
UDP 到底怎么实现可靠的
前端
Java后端的Ai之路8 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
Wang's Blog8 小时前
Java 项目实战: 交友项目 - 阿里云 OSS 入门案例:从创建 Bucket 到上传拿到可访问 URL
java·阿里云·交友