返回首页 Scala 抽象成员

延迟初始化(Lazy vals)

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


object Demo {
    val x = { println("initializing x"); "done"}
}

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


scala> Demo
initializing x
res0: Demo.type = Demo$@78178c35

scala> Demo.x
res1: String = done

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


object Demo {
    lazy val x = { println("initializing x"); "done"}
}

defined object Demo

scala> Demo
res0: Demo.type = Demo$@7de1c412

scala> Demo.x
initializing x
res1: String = done

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

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


trait LazyRationalTrait{
    val numerArg :Int
    val denomArg :Int

    lazy val numer = numerArg/g
    lazy val denom = denomArg/g

    private lazy val g = {
        require(denomArg !=0)
        gcd(numerArg,denomArg)
    }
    private def gcd(a:Int,b:Int):Int =
        if(b==0) a else gcd(b, a % b)

    override def toString = numer + "/" + denom
}

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


scala> val x = 2
x: Int = 2

scala> new LazyRationalTrait{
    val numerArg = x
    val denomArg = 2 * x
}

res2: LazyRationalTrait = 1/2

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

  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构造出来并显示。

在这个例子中,我们在写代码时,g 定义在 number 和 denom 的后面,然而,由于这三个变量都是使用 lazy 来定义的,因此它们在代码中出现的顺序并不重要。