scala雾中风景(12): App特质的延迟初始化

一个同事的问题,下面的代码运行时引用的外部变量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就正常了。

scala雾中风景(12): App特质的延迟初始化》上有5条评论

    1. hongjiang 文章作者

      就是一个普通的trait,它里面定义了main方法,会调用子类构造体里的逻辑。

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注