一个同事的问题,下面的代码运行时引用的外部变量TestClosure.count
没有初始化:
object TestClosure extends App {
val words = Array("a","ab","abc")
val count = 10
val cnt = words.map{word => (word, count)}
cnt.foreach(println)
}
object TestRef extends App {
//对应上面map里面那个匿名函数
val c = Class.forName("TestClosure$$anonfun$1")
val meod = c.getDeclaredMethod("apply", classOf[String])
val res = meod.invoke(c.newInstance(), "zhang")
// (zhang,0) 并不是 (zhang,10),说明外层object的count并没有被赋值
println(res)
}
如果运行 TestClosure
是ok的:
$ scala TestClosure
(a,10)
(ab,10)
(abc,10)
但是运行 TestRef
时,发现引用的TestClosure
里的count
变量没有被赋值:
$ scala TestRef
(zhang,0)
这个问题咋一看以为是闭包上下文绑定问题,实际上与闭包无关,是因为继承了App
特质导致的,看一下App
特质:
trait App extends DelayedInit
DelayedInit
特质里定义了延迟初始化方法:
def delayedInit(x: => Unit): Unit
scala在运行时同java,由一个包含main方法的单例作为入口,大概是2.8的时候为了简便,设计了App
特质,由App
提供main
方法,用户可以直接在初始化块里写逻辑,然后编译器会把这段初始化代码块里的逻辑封装成一个函数对象缓存起来(并没有运行),只有在执行到main
方法的时候才会触发。
通过一个简单的例子看一下,首先看没有继承App
特质的情况:
object Foo {
val count = 10
println(count)
}
上面的代码,在翻译为java时,成员的赋值,以及println
都是在构造函数或构造块里执行的
class Foo$ {
private final int count;
private Foo$(){
count = 10;
println(count);
}
// 忽略getter等其他不相关内容
}
再看看 Foo
继承自 App
之后:
object Foo extends App {
val count = 10
println(count)
}
翻译成java代码时,构造函数里相当于:
class Foo$ implements App {
private final int count;
private Foo$(){
// 逻辑被封装起来,延迟到main方法时才执行
delayedInit( anonymousFunction{count = 10; println(count)});
}
}
逻辑并没有被执行,而是封装在initCode
这个Buffer
里:
/** The init hook. This saves all initialization code for execution within `main`.
* This method is normally never called directly from user code.
* Instead it is called as compiler-generated code for those classes and objects
* (but not traits) that inherit from the `DelayedInit` trait and that do not
* themselves define a `delayedInit` method.
* @param body the initialization code to be stored for later execution
*/
override def delayedInit(body: => Unit) {
initCode += (() => body)
}
只有main
方法执行时,才会触发这些逻辑,见App.main
:
def main(args: Array[String]) = {
...
for (proc <- initCode) proc()
...
}
所以原因就在TestClosure
这个单例继承了App
导致内部的逻辑延迟初始化,取消继承App
就正常了。