Scala类中的val和object是什么?

33

在Scala类中,声明字段为vallazy valobject有什么区别?例如下面的代码片段:

class A

class B {
  val a1 = new A      { def foo = 1 }
  object a2 extends A { def foo = 1 }
  lazy val a3 = new A { def foo = 1 }
}

原来还应该将lazy val a3 = new A { def foo = 1 }也添加到问题中。 - PeWu
请参阅Scala - new vs object extends - Bergi
7个回答

22

在前者中,任何包含的代码都会在B类创建时立即执行。然而,在后者中,除非你实际使用该对象,否则它不会被实例化。

你可以在这里看到区别:

class A { println("Creating a new A") }
class B {
  val a1 = new A { println("a1"); def foo = 1 }
  object a2 extends A { println("a2"); def foo = 1 }
}

scala> val b = new B
Creating a new A
a1
b: B = B@1176e8a

scala> b.a2.foo
Creating a new A
a2
res0: Int = 1

创建的 .class 文件的命名等方面也存在隐藏的差异;当然,这两种类型也是不同的。


1
那么 object 的工作方式类似于 lazy val。这两者之间有任何实际区别吗? - PeWu
2
就我所知,从本质上讲不是这样的。object 的字节码更加紧凑一些。我不确定这是否意味着 lazy val 的编写效率低下,或者在某些线程条件下 object 可能不安全,或者两者都有可能。 - Rex Kerr
或者并不是我当时记得的那样。请查看Alex Boisvert的评论以了解关键差异! - Rex Kerr
哎呀!这两个差异让我想起了C++的陷阱(一个对象就像一个val,除了它是懒加载的,而且当它是类的字段并且被继承时,它不能被覆盖。哎呀。也许我们应该使用lazy val代替对象! - Elazar Leibovich
3
就我所知,在我读过的Scala代码中,懒惰变量似乎比对象成员更受欢迎。如果你在代码中看到一个对象成员,那很可能是在2.6 Scala之前编写的。因为在那个版本之后,懒惰变量被引入并广泛使用。 - Dave Griffith
1
实际区别:调用对象中定义的方法比调用匿名类中定义的方法更有效率。 - Aaron Novstrup

19

我不确定aioobe是否认识到他的答案的重要性,但不同类型实际上代表了valsobjects之间的关键差异。特别地,vallazy val具有结构类型(例如A{def foo: Int}),而object具有单例类型。因此,在val上调用foo方法会涉及反射,而在object上调用foo方法则不会:

class A

class B {
  val a1 = new A      { def foo = printStack }
  object a2 extends A { def foo = printStack }
  lazy val a3 = new A { def foo = printStack }

  def printStack() = 
     new Exception().getStackTrace take 3 foreach println
}

scala> val b = new B
b: B = B@5c750

scala> b.a1.foo   // the val
line124$object$$iw$$iw$B.printStack(<console>:12)
line124$object$$iw$$iw$B$$anon$1.foo(<console>:7)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

scala> b.a2.foo   // the object
line124$object$$iw$$iw$B.printStack(<console>:12)
line124$object$$iw$$iw$B$a2$.foo(<console>:8)
line128$object$$iw$$iw$.<init>(<console>:9)

scala> b.a3.foo   // the lazy val
line124$object$$iw$$iw$B.printStack(<console>:12)
line124$object$$iw$$iw$B$$anon$2.foo(<console>:9)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

哎呀!我不知道匿名类使用反射。我猜在 pimp-my-library 模式中使用它们最好避免,最好定义一个命名类。匿名类在 scala-swing 中被广泛使用,但速度并不重要。 - Luigi Plinge
3
匿名类只会在父类/接口中没有定义某个方法时才使用反射。如果A已经定义了foo方法,即使是抽象的定义,就不需要使用反射了。 - Aaron Novstrup

18

一个主要的区别是 val 可以被覆盖,而对象不能。

class C extends B {                           
  override val a1 = new A { def foo = 2 }     
  override object a2 extends A { def foo = 2 }
}

导致:

<console>:9: error: overriding object a2 in class B of type object C.this.a2;
object a2 cannot be used here - classes and objects cannot be overridden
override object a2 extends A { def foo = 2 }

3

我想不同之处在于a1将属于A的一个子类型,而a2将属于A的另一个子类型,即a2.type

scala> class A
defined class A

scala> val a1 = new A {def foo = 1}
a1: A{def foo: Int} = $anon$1@a9db0e2

scala> object a2 extends A {def foo = 1}
defined module a2

scala> a1
res0: A{def foo: Int} = $anon$1@a9db0e2

scala> a2
res1: a2.type = a2$@5b25d568

scala> 

当然可以,但它们将不是相同的类型。 - aioobe
1
更准确地说,a2将具有类型a2.type。 - venechka
1
请注意,您的Scala示例输出与您在答案的第一句话中所说的相矛盾:a1的类型不是 A;该类型是您创建的匿名子类的类型。您可以通过显式指定类型来将其设置为Aval a1: A = new A {def foo=1} - Jesper
@Jesper:刚刚注意到这个问题。a1的类型并不是匿名子类的类型;它是结构类型A{def foo: Int},与匿名子类不同之处在于调用foo将使用反射。您可以通过让foo打印堆栈跟踪来验证这一点。 - Aaron Novstrup
@Aaron:是的,没错。而且因为它将使用反射调用,所以调用速度比普通方法调用要慢得多。 - Jesper
显示剩余2条评论

3
另一个主要区别是对象知道自己的名称,而val则不知道。

2

第一个实用差别在于,lazy vals 和 objects 是惰性加载的,而 vals 是急切加载的。

对象和 lazy vals 的主要区别在于,从语言角度来看,对象被认为是“单例”的,而从 jvm 角度来看,它通常被视为静态成员。给定示例中的对象定义不能被重写,就像其他人已经证明的那样,因为没有被绑定到实例,所以没有可想象的方法进行虚函数查找。

object Foo { object Bar extends A; }

它大致像以下 Java 代码:

class Foo { 
  private static class Bar extends A{}
  public static Bar Bar = new Bar;
}

如果在上面的例子中,定义了一个子类C扩展Foo,它将无法覆盖Bar的定义。Java中的静态实例Bar将作为Foo.Bar访问。C.Bar的含义与(new C).Bar不同。我可能有点偏离主题,因为我实际上没有尝试反编译scala代码,这只是一个例子,用于说明对象作为静态成员的一般概念。 lazy val可能会稍微不那么高效。据我上次检查,它们通过维护类中的隐藏字段来实现,该字段跟踪已初始化哪些lazy val。维护此字段需要锁定,这可能会导致性能问题。 lazy val和object之间的一个主要实际差异是失败的处理方式:
如果我有:
class Foo() { throw new Exception("blah!"); }
object Stuff { object Bar extends Foo { val x = "hi" } }
Stuff.Bar
//exception "blah!" thrown.
Stuff.Bar.x
//NoClassDefFoundError: Could not initialize Stuff$Bar$

但如果我执行以下操作:

object Stuff2 { lazy val Bar = new Foo() { val x = "hi" } }
Stuff2.Bar
// "blah!"
Stuff2.Bar.x
// "blah!"

"NoClassDefFoundError" 可能会让人感到困惑,因为它是一个错误而不是异常,所以它可能会破坏捕获/记录“异常”但允许错误传播的错误处理代码。我甚至认为这种情况在Scala语言中算是一个缺陷,因为这种用例确实表明了一个异常情况,而不是真正的JVM错误。当访问依赖于外部资源(例如数据库连接或磁盘上的文件)的对象时,我曾经遇到过NoClassDefFoundErrors。只有第一次访问才记录了根本原因,因此正确调试此类问题通常需要重新启动应用程序服务器。

0

这不是结构类型:val a = new A { def foo = 1 }

它创建了一个唯一的匿名子类;a.foo在该类中调用foo。

x在此处是结构类型:def bar(x: { def bass: Int })

x.bass将检查x(未知类型)以查找名称为“bass”的方法。它适用于鱼或乐器。 ;)

lazy val和object之间的一个区别是:

var someA = (new B).a3
someA = (new B).a3 // ok

var anotherA = (new B).a2
anotherA =  = (new B).a2 // compile error

3
你之所以会出现编译错误,是因为你连续使用了两个等号。 - Luigi Plinge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接