Kotlin函数作为构造参数时未保留创建时envelope变量值问题排查
问题根因分析
1. 首先排除你猜测的几个问题
- envelope是
doSomething方法内定义的只读val,本身不会被重新赋值 - envelope被你传入ControllerTask的lambda捕获,只要lambda实例没有被GC,envelope就会被强引用持有,不会被提前回收
- envelope在ControllerTask创建时就已经完成赋值,不会出现未赋值的情况
2. 导致envelope执行时取值和创建时不一致的核心原因有两个
原因1:envelope是可变对象/浅拷贝对象,后续被修改
你调用request.toEnvelope()时,如果返回的envelope是request内部属性的浅拷贝,甚至是直接引用了request的内部可变对象,那么只要你在doSomething()执行完成后、ControllerTask.execute()执行前修改了request的对应属性,envelope指向的对象内部状态就会同步变化。
Kotlin的
val只保证变量的引用地址不可变,不保证引用指向的对象内部属性不可变。
原因2:ControllerTask的协程作用域实现错误,导致隐式并发问题
你重写的coroutineContext的get()方法每次调用都会返回全新的Job实例和调度器上下文,会引发两个问题:
- 每次调用
execute()时launch绑定的都是临时生成的独立作用域,任务的执行时序完全不可控,可能出现多个execute()调用的任务并发修改共享变量的情况 - 作用域没有统一的根Job管理,如果你有多个
ControllerTask任务同时执行,且共享envelope相关的资源,很容易出现读写冲突
// 错误实现:每次访问都生成新的上下文 override val coroutineContext: CoroutineContext get() = Job() + Dispatchers.Default
修复方案
- 第一步:修改
toEnvelope()实现,返回深拷贝的不可变Envelope实例,彻底切断和原request的关联 - 第二步:修复
ControllerTask的协程作用域实现,上下文只初始化一次:
class ControllerTask( private val task: suspend () -> Response ) : CoroutineScope { // 只初始化一次作用域上下文,共用同一个根Job override val coroutineContext: CoroutineContext = Job() + Dispatchers.Default fun execute() { launch(Dispatchers.IO) { val response = task() } } }
- 如果需要确认
envelope的状态,可以在创建ControllerTask时就打印envelope的核心属性,和execute()执行时打印的属性做对比,就能定位是赋值前还是赋值后被修改。
内容的提问来源于stack exchange,提问作者Marco7757
相关产品推荐
相关产品推荐

