我们知道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 至少试图帮助我们避免这种情况。