Scala 专题教程-抽象成员(5): 延迟初始化(Lazy vals)

jerry Scala 2015年11月25日 收藏

除了前面介绍的预先初始化成员值外,你还是让系统自行决定何时初始化成员的初始值,这是通过在val 定义前面添加lazy(懒惰),也是说直到你第一次需要引用该成员是,系统才会去初始化,否则该成员就不初始化(这也是lazy的由来:-)).
首先我们定义一个正常定义val的例子:

  1. object Demo {
  2. val x = { println("initializing x"); "done"}
  3. }

我们首先引用Demo,然后Demo.x

  1. scala> Demo
  2. initializing x
  3. res0: Demo.type = Demo$@78178c35
  4.  
  5. scala> Demo.x
  6. res1: String = done
  7.  

正如你所看到的,当引用Demo对象时,它的成员x也会初始化,初始化x伴随着初始化Demo的过程。然后,如果我们在val x前添加 lazy ,情况就有所不同了:

  1. object Demo {
  2. lazy val x = { println("initializing x"); "done"}
  3. }
  4.  
  5. defined object Demo
  6.  
  7. scala> Demo
  8. res0: Demo.type = Demo$@7de1c412
  9.  
  10. scala> Demo.x
  11. initializing x
  12. res1: String = done
  13.  

在使用lazy之后,初始化Demo时,不会初始化x,只有在引用到Demo.x该初始化代码才会执行。
这有点类似定义了一个无参数的方法,但和def不同的是,lazy变量初始化代码只会执行一次。
通过这个例子,我们可以看到例如Demo的对象本身也像一个lazy变量,也是在第一次引用时才会初始化,这是正确的,实际上一个object定义可以看成是使用了lazy val定义一个匿名类实例的简化方式。

使用lazy val,我们可以修改之前的RationalTrait, 在这个新的Trait定义中,所有的类成员变量的实现(非抽象成员)都使用lazy来修饰。

  1. trait LazyRationalTrait{
  2. val numerArg :Int
  3. val denomArg :Int
  4. lazy val numer = numerArg/g
  5. lazy val denom = denomArg/g
  6. private lazy val g = {
  7. require(denomArg !=0)
  8. gcd(numerArg,denomArg)
  9. }
  10. private def gcd(a:Int,b:Int):Int =
  11. if(b==0) a else gcd(b, a % b)
  12. override def toString = numer + "/" + denom
  13. }

同时我们把require移动到g 里面,这样所有的lazy val初始化代码都移动到val定义的右边。我们不再需要预先初始化成员变量。测试如下:

  1. scala> val x = 2
  2. x: Int = 2
  3.  
  4. scala> new LazyRationalTrait{
  5. val numerArg = x
  6. val denomArg = 2 * x
  7. }
  8.  
  9. res2: LazyRationalTrait = 1/2
  10.  

我们来分析一下这段代码中命令行的执行顺序:

  1. 首先,创建了一个新的LazyRationalTrait的实例,执行LazyRationalTrait的初始化代码,这部分代码为空,LazyRationalTrait 所有成员变量都没有初始化。
  2. 其次,该Trait的匿名子类的主构造函数被执行,这部分初始化numberArg 和 denomArg为2和4.
  3. 接下来,命令行需要调用该实例的toString方法来显示该实例的值。
  4. 接下来,toString需要访问成员number这是第一次访问该成员,因此lazy val初始化代码被执行。初始化代码调用私有成员g,因此需要计算g的值,用到之前定义过的numberArg 和 denomArg。
  5. 接下来toString需要访问成员denom这是第一次访问该成员,因此lazy val初始化代码被执行。初始化代码调用私有成员g,因此需要计算g的值,因为g已经计算过,无需再计算。
  6. 最后,toString的结果1/2构造出来并显示。