为什么变量不能像在 java 中那样在内联函数中正确初始化?



我们知道lambda体是懒惰的,因为如果我们不调用lambda,lambda体中的代码永远不会被调用。

我们还知道在任何函数语言中,变量即使没有初始化也可以在函数/lambda 中使用,例如 javascript、ruby、groovy 和 .etc,例如下面的 groovy 代码可以正常工作:

def foo
def lambda = { foo }
foo = "bar"
println(lambda())
//      ^--- return "bar"

我们还知道,当 Java 中的 try-block 引发异常时,如果 catch-block 初始化了变量,我们可以访问未初始化的变量,例如:

//  v--- m is not initialized yet
int m;
try{ throw new RuntimeException(); } catch(Exception ex){ m = 2;}
System.out.println(m);// println 2

如果 lambda 是懒惰的,为什么 Kotlin 不能在 lambda 中使用未初始化的变量?我知道 Kotlin 是一种空安全语言,因此编译器将从上到下分析代码,包括 lambda 正文,以确保变量已初始化。因此,lambda 主体在编译时不会"懒惰"。例如:

var a:Int
val lambda = { a }// lambda is never be invoked
//             ^--- a compile error thrown: variable is not initialized yet
a = 2

:但是为什么下面的代码也无法正常工作?我不明白,因为变量在 Java 中实际上是最终的,如果你想改变变量值,你必须使用ObjectRef代替,这个测试与我之前的结论相矛盾:"lambda body 在编译时不是懒惰的".例如:

var a:Int
run{ a = 2 }// a is initialized & inlined to callsite function
//      v--- a compile error thrown: variable is not initialized yet
println(a)

所以我只能想到的是编译器无法确定ObjectRef中的element字段是否初始化,但@hotkey否认了我的想法。为什么

:为什么 Kotlin 内联函数不能正常工作,即使我像在 Java 中那样在 catch-block 中初始化变量? 例如:

var a: Int
try {
run { a = 2 }
} catch(ex: Throwable) {
a = 3
}
//      v--- Error: `a` is not initialized
println(a)

但是,@hotkey已经提到,你应该在 Kotlin 中使用try-catch表达式来初始化他的答案中的变量,例如:

var a: Int = try {
run { 2 }
} catch(ex: Throwable) {
3
}
//      v--- println 2
println(a);

:如果实际情况是这样,为什么我不直接打电话给run? 例如:

val a = run{2};
println(a);//println 2

但是,上面的代码可以在java中正常工作,例如:

int a;
try {
a = 2;
} catch (Throwable ex) {
a = 3;
}
System.out.println(a); // println 2

问:但是为什么下面的代码也无法正常工作?

因为代码可以改变。在定义 lambda 的位置,变量不会初始化,因此如果更改代码并且之后直接调用 lambda,它将无效。kotlin 编译器希望确保在初始化之前绝对无法访问未初始化的变量,即使是通过代理也是如此。

问:为什么 Kotlin 内联函数不能正常工作,即使我像在 java 中那样在 catch-block 中初始化变量?

因为run不是特殊的,编译器无法知道主体何时执行。如果您考虑run未执行的可能性,则编译器无法保证变量将被初始化。

在更改的示例中,它使用 try-catch 表达式本质上执行a = run { 2 },这与run { a = 2 }不同,因为结果由返回类型保证。

问:如果实际情况是这样,为什么我不直接调用运行?

这基本上就是发生的事情。关于最终的 Java 代码,事实是 Java 不遵循与 Kotlin 完全相同的规则,并且反过来也会发生同样的情况。仅仅因为在 Java 中某些东西是可能的并不意味着它将是有效的 Kotlin。

您可以使用以下内容使变量延迟...

val a: Int by lazy { 3 }

显然,您可以使用函数代替 3。 但这允许编译器继续,并保证a在使用前初始化。

编辑

尽管问题似乎是"为什么不能做到"。 我处于相同的思维框架中,我不明白为什么不(在合理范围内(。 我认为编译器有足够的信息来确定 lambda 声明不是对任何闭包变量的引用。 因此,我认为当使用 lambda 并且它引用的变量尚未初始化时,它可能会显示不同的错误。

也就是说,如果编译器编写者不同意我的评估(或者需要很长时间才能绕过该功能(,我会这样做。

以下示例显示了执行延迟局部变量初始化的方法(适用于版本 1.1 及更高版本(

import kotlin.reflect.*
//...
var a:Int by object {
private var backing : Int? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): Int =
backing ?: throw Exception("variable has not been initialized") 
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
backing = value
}
}
var lambda = { a }
// ...
a = 3
println("a = ${lambda()}")

我使用了一个匿名对象来显示正在发生的事情的胆量(并且因为lazy导致了编译器错误(。 该对象可以像lazy一样变成函数。

现在,如果程序员忘记在引用变量之前初始化变量,我们可能会回到运行时异常。 但 Kotlin 至少试图帮助我们避免这种情况。

最新更新