繁体   English   中英

为什么每个新的Case类实例在Scala中再次评估惰性值?

[英]Why do each new instance of case classes evaluate lazy vals again in Scala?

据我了解,scala将val定义视为值。 因此,具有相同参数的案例类的任何实例都应该相等。 但,

case class A(a: Int) {
   lazy val k = {
       println("k")
       1
   }

 val a1 = A(5)
 println(a1.k)
 Output:
 k
 res1: Int = 1

 println(a1.k)
 Output:
 res2: Int = 1

 val a2 = A(5)
 println(a1.k)
 Output:
 k
 res3: Int = 1

我期望对于println(a2.k),它不应该打印k。 由于这不是必需的行为,因此我应该如何实现, 以便对于具有相同参数的case类的所有实例,只应执行一次惰性val定义一次。 我需要一些记忆技术还是Scala可以自己处理?

我对Scala和函数式编程非常陌生,所以如果您发现此问题微不足道,请原谅。

假设您没有重写equals或进行诸如构造函数args var s之类的不良建议,那么情况是两个具有相同构造函数参数的case类实例化将相等。 但是,这并不意味着与同一构造函数参数的两个案例类实例将指向内存中的同一个对象

case class A(a: Int)
A(5) == A(5)  // true, same as `A(5).equals(A(5))`
A(5) eq A(5)  // false

如果您希望构造函数始终在内存中返回相同的对象 ,则需要自己处理。 也许使用某种工厂:

case class A private (a: Int) {
  lazy val k = {
    println("k")
    1
  }
}

object A {
  private[this] val cache = collection.mutable.Map[Int, A]()
  def build(a: Int) = {
    cache.getOrElseUpdate(a, A(a))
  }
}

val x = A.build(5)
x.k                  // prints k
val y = A.build(5)
y.k                  // doesn't print anything
x == y               // true
x eq y               // true

相反,如果您不关心构造函数返回相同的对象,而只关心k的重新计算,则可以缓存该部分:

case class A(a: Int) {
  lazy val k = A.kCache.getOrElseUpdate(a, {
    println("k")
    1
  })
}

object A {
  private[A] val kCache = collection.mutable.Map[Int, Int]()
}

A(5).k     // prints k
A(5).k     // doesn't print anything

简单的答案是“这是语言根据规范所做的”。 这是正确的,但不是很令人满意的答案。 为什么这样做会更有趣。

可能更清楚,它必须使用另一个示例来执行此操作:

case class A[B](b: B) {
   lazy val k = {
       println(b)
       1
   }
}

当构造两个A ,您不知道它们是否相等,因为尚未定义它们相等意味着什么(或B相等意味着什么)。 而且您也不能静态初始化k ,因为它取决于传入的B

如果必须打印两次,那么只有在k依赖于b的情况下才是完全直观的,而如果它不依赖于b话则不是那么直观。

当你问

我应该如何实现它,以便对于具有相同参数的case类的所有实例,只应执行一次惰性val定义一次

这是一个听起来棘手的问题。 您使“相同参数”听起来像在编译时就可以知道的东西,而无需进一步的信息。 不是,您只能在运行时知道它。

而且,如果您仅在运行时知道这一点,则意味着您必须保留实例A[B]所有过去使用。 这是内置的内存泄漏-难怪Scala没有内置的方法可以做到这一点。

如果您确实要这样做-并仔细考虑内存泄漏-构造一个Map[B, A[B]] ,然后尝试从该映射中获取一个缓存的实例,如果它不存在,则构造一个并把它放在地图上

我相信case class仅将其构造函数(而不是任何辅助构造函数)的参数视为其相等性概念的一部分。 想想当你在使用的情况下,类match的语句, unapply只给你访问(默认情况下)的构造函数的参数。

将案例类主体中的任何内容视为“额外”或“副作用”。 我认为这是一个很好的策略,使案例类尽可能接近空,并将任何自定义逻辑放入伴随对象中。 例如:

case class Foo(a:Int)

object Foo {
    def apply(s: String) = Foo(s.toInt)
}

除了dhg答案外,我应该说,我不知道默认情况下不会进行完整构造函数记忆的功能语言。 您应该理解,这种记忆意味着所有构造的实例都应保留在内存中,这并不总是可取的。

手动缓存并不难,请考虑以下简单代码

import scala.collection.mutable

class Doubler private(a: Int) {
  lazy val double = {
    println("calculated")
    a * 2
  }
}

object Doubler{
  val cache = mutable.WeakHashMap.empty[Int, Doubler]
  def apply(a: Int): Doubler = cache.getOrElseUpdate(a, new Doubler(a))
}

Doubler(1).double   //calculated

Doubler(5).double   //calculated

Doubler(1).double   //most probably not calculated

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM