为什么不应该让每个Scala实例变量成为一个懒惰的初始化变量?

Rad*_*scu 9 scala

除了增加的详细程度之外,还有其他强有力的理由说明为什么不应该声明每个实例变量应该被懒惰地初始化?

Rüd*_*ehn 20

首先:如果懒惰的初始化出现问题(比如访问不存在的外部资源),你只会在第一次访问val时注意到它,而使用普通的val你会很快注意到正在构建对象.您还可以在延迟val中具有循环依赖性,这将导致该类根本不起作用(一个可怕的NullPointerExceptions),但是您只能在第一次访问其中一个连接的惰性值时找到它.

所以懒惰的vals使程序不那么确定,这总是一件坏事.

第二:懒惰的val涉及运行时开销.懒惰的val目前由一个使用延迟val的类中的私有位掩码(int)实现(每个懒惰的val有一位,所以如果你有超过32个懒的val,那么将有两个位掩码等)

为了确保延迟val初始值设定项仅运行一次,在初始化字段时会对位掩码进行同步写入,并且每次访问该字段时都会进行易失性读取.现在,在x86架构中,易失性读取相当便宜,但是易失性写入可能非常昂贵.

据我所知,在未来版本的scala中正在努力优化这一点,但是与直接访问相比,检查字段是否已初始化总会有开销.例如,lazy val访问的额外代码可能会阻止方法被内联.

当然对于非常小的类,位掩码的内存开销也可能是相关的.

但即使你没有任何性能问题,最好弄清楚val相互依赖的顺序,然后按顺序对它们进行排序并使用正常的val.

编辑:这是一个代码示例,说明了使用延迟val时可能获得的非确定性:

class Test {
  lazy val x:Int = y
  lazy val y:Int = x
}
Run Code Online (Sandbox Code Playgroud)

您可以毫无问题地创建此类的实例,但只要您访问x或y,就会获得StackOverflow.这当然是一个人为的例子.在现实世界中,你有更长的和非明显的依赖周期.

这是一个使用scala控制台会话:javap,它说明了lazy val的运行时开销.首先是正常的val:

scala> class Test { val x = 0 }
defined class Test

scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public int x();
  Code:
   0:   aload_0
   1:   getfield    #11; //Field x:I
   4:   ireturn

public Test();
  Code:
   0:   aload_0
   1:   invokespecial   #17; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   iconst_0
   6:   putfield    #11; //Field x:I
   9:   return

}
Run Code Online (Sandbox Code Playgroud)

而现在懒惰的val:

scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public volatile int bitmap$0;

public int x();
  Code:
   0:   aload_0
   1:   getfield    #12; //Field bitmap$0:I
   4:   iconst_1
   5:   iand
   6:   iconst_0
   7:   if_icmpne   45
   10:  aload_0
   11:  dup
   12:  astore_1
   13:  monitorenter
   14:  aload_0
   15:  getfield    #12; //Field bitmap$0:I
   18:  iconst_1
   19:  iand
   20:  iconst_0
   21:  if_icmpne   39
   24:  aload_0
   25:  iconst_0
   26:  putfield    #14; //Field x:I
   29:  aload_0
   30:  aload_0
   31:  getfield    #12; //Field bitmap$0:I
   34:  iconst_1
   35:  ior
   36:  putfield    #12; //Field bitmap$0:I
   39:  getstatic   #20; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
   42:  pop
   43:  aload_1
   44:  monitorexit
   45:  aload_0
   46:  getfield    #14; //Field x:I
   49:  ireturn
   50:  aload_1
   51:  monitorexit
   52:  athrow
  Exception table:
   from   to  target type
    14    45    50   any

public Test();
  Code:
   0:   aload_0
   1:   invokespecial   #26; //Method java/lang/Object."<init>":()V
   4:   return

}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,普通的val访问器非常短并且肯定会内联,而惰性val访问器非常复杂(并且最重要的是并发)涉及同步块(monitorenter/monitorexit指令).您还可以看到编译器生成的额外字段.


mik*_*łak 7

首先,我们应该讨论lazy vals(Scala的"常量"),而不是懒惰变量(我认为不存在).

两个原因是可维护性和效率,特别是在类字段的上下文中:

效率:非懒惰初始化的好处是你可以控制它发生的位置.想象一下fork-join类型框架,在这个框架中,您可以在工作线程中生成许多对象,然后将它们交给中央处理.通过eager eval,初始化在工作线程上完成.使用延迟eval,这是在主线程上完成的,可能会产生瓶颈.

可维护性:如果您的所有值都被懒惰地初始化,并且您的程序爆炸,您将获得一个堆栈跟踪,该跟踪本地化在与实例初始化完全不同的上下文中,可能在另一个线程中.

几乎可以肯定的是,与语言实现相关的成本(我看到@Beryllium已经发布了一个例子),但我觉得没有足够的能力来讨论它们.