Kotlin闭包中如何避免原列表遍历期间被修改?
Kotlin闭包与列表遍历的一致性问题
我对Kotlin的闭包特性不太了解,现在有个问题想搞清楚:我定义了一个Foo类,里面有个bar成员,是个技术上不可变的列表——修改时会创建新列表替换掉Foo.bar。之后我启动一个协程遍历这个列表,把Foo.bar传入闭包,但遍历过程耗时很长(中间有延迟),遍历期间Foo.bar的内容可能被修改。我想知道:
- 如何确保遍历能稳定输出0到10?
- 闭包本身能保证这一点吗?
- 需要在传入闭包前对列表做逐元素深拷贝吗?
相关代码
class Foo(var bar: List<Int>) suspend fun <T> Collection<T>.traverseOverTime(block: (T) -> Unit) { this.iterator().traverseOverTime(block) } suspend fun <T> Iterator<T>.traverseOverTime(block: (T) -> Unit) { repeat(2) { if (!hasNext()) return block(next()) } delay(1000) traverseOverTime(block) } fun main() = runBlocking { val foo = Foo(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) launch { foo.bar.traverseOverTime { println(it.toString()) } } // case 1.: 尝试强转成可变列表修改元素 delay(500) try { (foo.bar as MutableList<Int>)[2] = 1000 println("case 1.: ${foo.bar}") } catch (e: Exception) { println("case 1.: $e") } // case 2.: 尝试强转成可变列表删除元素 delay(1000) try { (foo.bar as MutableList<Int>).removeAt(3) println("case 2.: ${foo.bar}") } catch (e: Exception) { println("case 2.: $e") } // case 3.: 直接替换Foo.bar指向新列表 delay(1000) foo.bar = listOf(100) println("case 3.: ${foo.bar}") }
输出结果
0 1 case 1.: [0, 1, 1000, 3, 4, 5, 6, 7, 8, 9, 10] 1000 3 case 2.: java.lang.UnsupportedOperationException 4 5 case 3.: [100] 6 7 8 9 10
问题解答
1. 闭包是否能保证遍历原始列表?
闭包已经捕获了协程启动时的那个列表实例,所以即使后续Foo.bar被赋值为新列表,遍历依然会使用最初的那个旧列表——这就是为什么case3替换Foo.bar后,遍历还是输出了6到10。
但闭包无法阻止你对原始列表实例本身的修改,这也是case1中遍历读到1000的原因。
2. 为什么case1能修改元素,case2却失败?
Kotlin的listOf()返回的是一个内部实现类(类似ArrayList),它对外暴露不可变的List接口,但底层允许修改元素值,却不允许修改列表结构(比如增删元素)。强制转MutableList属于不安全操作,破坏了不可变约定:
- case1修改元素值成功,是因为底层实现支持该操作;
- case2尝试删除元素时,底层实现会抛出
UnsupportedOperationException,拒绝修改结构。
3. 如何确保遍历稳定输出0到10?
- 杜绝强制类型转换修改列表:永远不要把
List强转成MutableList去修改,违反不可变约定的操作会导致遍历结果不可控; - 使用真正不可变的列表:如果要彻底防止原始列表被修改,可以用
Collections.unmodifiableList()创建真正不可变的列表,这样强制转MutableList修改时会直接抛出异常; - 不需要深拷贝:你的例子中列表元素是不可变的
Int,闭包已经捕获了原始列表实例,只要这个实例本身不被修改,遍历就会输出原始数据。只有当列表元素是可变对象时,才需要考虑深拷贝。
优化后的代码示例
如果要保证遍历输出0到10,可将列表创建改为真正不可变:
val foo = Foo(Collections.unmodifiableList(mutableListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)))
此时case1的强制转换修改会直接抛出异常,遍历将输出原始的2而非1000。
内容的提问来源于stack exchange,提问作者Giancarlo
相关产品推荐
相关产品推荐

