Scala 中的 def’vs’val’vs’惰性 val’评估

我没理解错吧

  • 每次访问 def时都会对其进行计算

  • lazy val一旦被访问就会被计算

  • val一旦进入执行范围就会被评估吗?

25231 次浏览

是的,不过对于第三个,我会说“当这个语句被执行时”,因为,例如:

def foo() {
new {
val a: Any = sys.error("b is " + b)
val b: Any = sys.error("a is " + a)
}
}

这就是 "b is null"。从不计算 b,也不抛出它的错误。但一旦控制权进入该区块,它就在范围之内。

你是对的。从 说明书的证据:

摘自「3.3.1方法类型」(适用于 def) :

无参数方法命名每次重新计算的表达式 引用无参数方法名称。

摘自“4.1价值声明与定义”:

值定义 val x : T = ex定义为从 e的评价。

惰性值定义首先计算其右侧 e 访问该值的时间。

是的,但是有一个很好的技巧: 如果您有惰性值,并且在第一次计算时它将得到一个异常,下次您尝试访问它时,它将尝试重新计算自身。

下面是一个例子:

scala> import io.Source
import io.Source


scala> class Test {
| lazy val foo = Source.fromFile("./bar.txt").getLines
| }
defined class Test


scala> val baz = new Test
baz: Test = Test@ea5d87


//right now there is no bar.txt


scala> baz.foo
java.io.FileNotFoundException: ./bar.txt (No such file or directory)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:137)
...


// now I've created empty file named bar.txt
// class instance is the same


scala> baz.foo
res2: Iterator[String] = empty iterator

def定义了一个方法。当你调用这个方法时,这个方法当然会运行。

val定义一个值(一个不可变的变量)。赋值表达式在初始化该值时计算。

lazy val定义了一个具有延迟初始化的值。它将在第一次使用时被初始化,因此赋值表达式将在那时被求值。

应该指出在使用直到运行时才知道的值时使用 val 的潜在缺陷。

request: HttpServletRequest为例

如果你说:

val foo = request accepts "foo"

当初始化 Val时,您会得到一个 null 指针异常,request 没有 foo (只有在运行时才知道)。

因此,根据访问/计算的开销,def 或惰性 val 对于运行时确定的值来说是合适的选择; 或者 val 本身是一个检索运行时数据的匿名函数(尽管后者似乎有点边缘化)

选择 def而不是 val的一个很好的理由是,特别是在抽象类中(或者在用于模拟 Java 接口的特性中) ,你可以在子类中用 val覆盖 def,但反过来就不行了。

关于 lazy,有两件事情我可以看到,一个应该在心里。首先,lazy引入了一些运行时开销,但是我想您需要对您的具体情况进行基准测试,以确定这是否真的对运行时性能有重大影响。lazy的另一个问题是它可能会延迟引发异常,这可能会使得对程序进行推理变得更加困难,因为这个异常不是在第一次使用时引发的。

通过每次在程序中出现名称时替换名称及其 RHS 表达式来计算 def 限定的名称。因此,这个替换将在程序中出现名称的每个地方执行。

当 control 到达其 RHS 表达式时,将立即计算 val 限定的名称。因此,每次名称出现在表达式中时,它都将被视为此计算的值。

使用惰性 val 限定的名称遵循与 val 限定相同的策略,但是它的 RHS 只有在控件首次使用该名称时才被计算

我想通过我在 REPL 中执行的例子来解释这些差异。我相信这个简单的例子更容易理解和解释概念上的差异。

在这里,我创建了一个 val result t1、一个惰性 val result t2和一个 def result t3,它们的类型都是 String。

A)

scala> val result1 = {println("hello val"); "returns val"}
hello val
result1: String = returns val

在这里,执行 println 是因为在这里计算了 result 1的值。因此,现在 result 1将始终引用它的值,即“返回 val”。

scala> result1
res0: String = returns val

现在,您可以看到 result 1现在引用了它的值。请注意,println 语句在这里不执行,因为在第一次执行 result 1时,已经计算了它的值。因此,现在开始,result t1将始终返回相同的值,println 语句将永远不会再次执行,因为已经执行了获取 result t1值的计算。

B)懒鬼

scala> lazy val result2 = {println("hello lazy val"); "returns lazy val"}
result2: String = <lazy>

正如我们在这里看到的,println 语句在这里没有执行,值也没有被计算出来。这就是懒惰的本质。

现在,当我第一次引用 result 2时,将执行 println 语句并计算和赋值。

scala> result2
hello lazy val
res1: String = returns lazy val

现在,当我再次引用 result 2时,这一次,我们将只看到它保存的值,println 语句不会被执行。从现在开始,result 2的行为将简单地像 val 一样,并始终返回其缓存的值。

scala> result2
res2: String = returns lazy val

C) . def

在 def 的情况下,必须在每次调用 result 3时计算结果。这也是我们在 scala 中将方法定义为 def 的主要原因,因为每次在程序中调用方法时,方法都必须计算并返回一个值。

scala> def result3 = {println("hello def"); "returns def"}
result3: String


scala> result3
hello def
res3: String = returns def


scala> result3
hello def
res4: String = returns def