kotlin 编译错误:无法智能强制转换为“< type>”,因为“< variable>”是一个局部变量,它被一个不断变化的闭包捕获

1tu0hz3e  于 7个月前  发布在  Kotlin
关注(0)|答案(6)|浏览(100)

为了简化我的真实的用例,让我们假设我想找到列表中的最大数:

var max : Int? = null
listOf(1, 2, 3).forEach {
    if (max == null || it > max) {
        max = it
    }
}

字符串
但是,编译失败,并出现以下错误:
智能强制转换为'Int'是不可能的,因为'max'是一个局部变量,由不断变化的闭包捕获
为什么在这个例子中,一个不断变化的闭包会阻止smartcast工作?

w8f9ii69

w8f9ii691#

一般来说,当一个可变变量在lambda函数闭包中被捕获时,智能强制转换不适用于该变量,无论是在lambda内部还是在创建lambda之后的声明范围内。
这是因为函数可能会从它的封闭作用域中逃逸出来,然后在不同的上下文中执行,可能会多次执行,也可能是并行执行。举个例子,考虑一个假设的函数List.forEachInParallel { ... },它对列表中的每个元素执行给定的lambda函数,但是是并行执行的。
编译器必须生成即使在这种严重情况下也保持正确的代码,因此它不会假设变量的值在null检查后保持不变,因此不能智能转换它。
然而,List.forEach非常不同,因为它是一个inline函数。内联函数的主体及其函数参数的主体(除非参数有noinlinecrossinline修饰符)在调用位置内联,因此编译器可以推断作为参数传递给内联函数的lambda中的代码,就像它直接写在调用方法体中一样。使智能铸造成为可能。
它可以,但目前,它没有。只是因为该功能尚未实现。有一个开放的问题:KT-7186

aiqt4smr

aiqt4smr2#

感谢Ilya对问题的详细解释!你可以像这样使用标准的for(item in list){...}表达式:

var max : Int? = null
val list = listOf(1, 2, 3)
for(item in list){
    if (max == null || item > max) {
        max = item
    }
}

字符串

hjzp0vay

hjzp0vay3#

这看起来像是一个编译器bug。
如果forEach中的内联lambda参数被标记为crossinline,那么我将预期编译错误,因为lambda表达式的并发调用的可能性。
考虑以下forEach实现:

inline fun <T> Iterable<T>.forEach(crossinline action: (T) -> Unit): Unit {
    val executorService: ExecutorService = ForkJoinPool.commonPool()
    val futures = map { element -> executorService.submit { action(element) } }
    futures.forEach { future -> future.get() }
}

字符串
如果没有crossinline修饰符,上面的实现将无法编译。没有它,lambda可能包含非本地返回,这意味着它不能以并发方式使用。
我建议创建一个问题:Kotlin (KT) | YouTrack

mefy6pfw

mefy6pfw4#

问题是foreach创建了多个闭包,每个闭包都访问同一个max,也就是var
如果在max == null检查之后、it > max之前的另一个闭包中,max被设置为null,会发生什么?
由于每个闭包理论上都可以独立工作(可能在多个线程上),但都访问相同的max,因此您不能保证它在执行期间不会更改。

23c0lvtd

23c0lvtd5#

由于这是在错误Smart cast to '<type>' is impossible, because '<variable>' is a local variable that is captured by a changing closure的顶部结果中,这里是一个对我有效的通用解决方案(即使闭包不是内联的):
显示此错误的示例:

var lastLine: String? = null
File("filename").useLines {
    lastLine = it.toList().last()
}

if(lastLine != null) {
    println(lastLine.length) // error! lastLine is captured by the useLines closure above
}

字符串
Fix:创建一个新的变量,这个变量不被闭包捕获:

var lastLine: String? = null
File("filename").useLines {
    lastLine = it.toList().last()
}

val finalLastLine = lastLine
if(finalLastLine != null) {
    println(finalLastLine.length)
}

a0x5cqrl

a0x5cqrl6#

根据您的使用情况,elvis-operator还可以帮助:

var max: Int? = null
listOf(1, 2, 3).forEach {
        if (max == null || it > max ?: Int.MIN_VALUE) {
            max = it
        }
    }

字符串
第一个条件(max == null)仅在列表可能确实包含Int.MIN_VALUE作为第一个条目时才是必要的。

相关问题