Scala中的def vs val vs lazy val评估


69

我理解正确吗

  • def 每次访问时都会进行评估

  • lazy val 一旦被访问就被评估

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




请注意,val不能随意扩展。如果您在基本特征中具有val a = 5val b = a + 1并尝试使用扩展它,val a = 6那么b将引发一些未定义的错误,而不是7 。
ayvango'1

Answers:


50

是的,尽管对于第三个,我会说“执行该语句时”,因为,例如:

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

这给了"b is null"b永远不会评估,并且永远不会引发错误。但是一旦控制进入块就在范围内。


92

是的,但是有一个不错的窍门:如果您的值比较懒惰,并且在第一次评估时会出现异常,那么下次您尝试访问它时,它将尝试重新评估自己。

这是示例:

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

顺便说一句,除非有必要相反地定义所有val,否则不是一个好主意吗?
伊万

6
@Ivan我怀疑这会降低性能,因为为了提供线程安全性,懒惰的val在代码中经过了双重检查才能转换。可能还有其他原因。
om-nom-nom

2
好。谢谢。这是关于lazy valvs的想法val。另一个问题是lazy valVS def。我有一个不可变的类,其中的某些属性是构造函数中传递的数据的纯函数。将它们公开为惰性val而不是defs是不是一个好主意(考虑结果是一小部分数据(如整数值),但需要一些时间来计算)?
伊万

4
@Ivan就在几天前,我已经介绍了这种情况,当时使用惰性val在代码中引入了死锁。当心在多线程代码中使用惰性值。关于您的最新问题,我想是的,这不仅是在某些情况下的高性能解决方案,而且是更具可读性的IMO(因为将阅读此代码的人将意识到这是一段代码,因此它是init的)。
om-nom-nom

39

我想通过我在REPL中执行的示例来解释这些差异,我相信这个简单的示例更容易掌握并解释概念上的差异。

在这里,我正在创建一个val result1,一个懒惰的val result2和一个def result3,它们每个都有一个String类型。

一种)。值

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

在这里,执行println的原因是在这里已计算出result1的值。因此,现在result1将始终引用其值,即“ returns val”。

scala> result1
res0: String = returns val

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

B)。懒惰的瓦尔

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

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

现在,当我第一次引用result2时,将执行println语句并计算和分配值。

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

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

scala> result2
res2: String = returns lazy val

C)。定义

如果是def,则每次调用result3时都必须计算结果。这也是我们在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

2
这是最好的解释。应该有更多的赞成票。
JohnnyHuo

11

选择defover的一个很好的理由val,尤其是在抽象类(或用于模仿Java接口的特征)中,可以def用a覆盖avalin子类,但是不能反过来。

关于lazy,我可以看到有两件事应该牢记。首先是lazy引入了一些运行时开销,但是我想您需要对特定情况进行基准测试,以了解这是否确实对运行时性能产生重大影响。另一个问题lazy是,它可能会延迟引发异常,这可能会使推理程序变得更困难,因为异常不是预先抛出而是仅在首次使用时抛出。


需要在延迟val和def之间比较运行时性能。对于非平凡的函数/值,同步成本将被计算成本所掩盖。WRT隐藏异常,这取决于用例,可能很好。例如,在上面的FifthElement代码中,用户可以在调用thirdElement之前检查isEmpty,并且永远不会看到异常(从技术上讲,他们应该检查大小以确保序列至少包含五个元素),但是如果使用val,则该异常对象创建后立即将其抛出。
Noel Yap 2012年

虽然我在概念上同意您的评论,但我认为您firthElement不是一个很好的例子来说明它。如果您考虑前置条件和后置条件,则seq(5)只有在序列包含至少六个元素的情况下,调用才显然是安全的-即使isEmpty是假,也不能保证。更糟糕的是,如果调用方fithElement不是最初创建对象的调用方,则它的义务是无法实现的,因为调用方缺乏对序列长度的意识。添加length函数将使您的示例有效。
Malte Schwerhoff 2012年

是的,这是一个充满错误的虚构示例。目的是展示val,lazy val和def之间的差异。我的真实场景是六角形网格中的交集对象。相交可以是以下两种中的一种:一种具有顶部,左下和右下相邻图块(即“ Y”相交点)或一种具有底部,左上和右上相邻图块(即上侧) -向下的“ Y”交点)。尝试访问不存在的相邻图块时应引发异常。因此,不能使用val。
Noel Yap 2012年

我已经用hasFifthElement替换了isEmpty。
Noel Yap 2012年

8

你是对的。从规范中获取证据:

从“ 3.3.1方法类型”(对于def):

无参数方法名称表达式,每次引用无参数方法名称时都会重新计算。

从“ 4.1值声明和定义”中:

值定义val x : T = e将定义x为的值,该值是通过对进行评估得出的e

e第一次访问值时,惰性值定义将评估其右侧。


4

def定义一个方法。当您调用该方法时,当然会运行该方法。

val定义一个值(一个不可变的变量)。值初始化时将评估赋值表达式。

lazy val定义延迟初始化的值。首次使用时将对其进行初始化,因此将对赋值表达式进行求值。


3

每当名称出现在程序中时,都会通过替换名称及其RHS表达式来评估由def限定的名称。因此,此替换将在程序中名称出现的任何位置执行。

当控件达到其RHS表达式时,将立即评估由val限定的名称。因此,每当名称出现在表达式中时,它将被视为此求值的值。

由惰性val限定的名称遵循与val限定相同的策略,不同之处在于仅当控件达到首次使用该名称的位置时才评估其RHS


1

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

举个例子 request: HttpServletRequest

如果您要说:

val foo = request accepts "foo"

val初始化时,您将得到一个空指针异常,请求没有foo(只能在运行时知道)。

因此,根据访问/计算,DEF或懒惰val的费用是那么对于运行时确定的值进行适当的选择; 或本身就是一个匿名函数的val,它检索运行时数据(尽管后者似乎有些极端情况)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.