简体   繁体   English

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

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

From what I have understood, scala treats val definitions as values. 据我了解,scala将val定义视为值。 So, any instance of a case class with same parameters should be equal. 因此,具有相同参数的案例类的任何实例都应该相等。 But, 但,

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

I was expecting that for println(a2.k), it should not print k. 我期望对于println(a2.k),它不应该打印k。 Since this is not the required behavior, how should I implement this so that for all instances of a case class with same parameters, it should only execute a lazy val definition only once. 由于这不是必需的行为,因此我应该如何实现, 以便对于具有相同参数的case类的所有实例,只应执行一次惰性val定义一次。 Do I need some memoization technique or Scala can handle this on its own? 我需要一些记忆技术还是Scala可以自己处理?

I am very new to Scala and functional programming so please excuse me if you find the question trivial. 我对Scala和函数式编程非常陌生,所以如果您发现此问题微不足道,请原谅。

Assuming you're not overriding equals or doing something ill-advised like making the constructor args var s, it is the case that two case class instantiations with same constructor arguments will be equal. 假设您没有重写equals或进行诸如构造函数args var s之类的不良建议,那么情况是两个具有相同构造函数参数的case类实例化将相等。 However, this does not mean that two case class instantiations with same constructor arguments will point to the same object in memory: 但是,这并不意味着与同一构造函数参数的两个案例类实例将指向内存中的同一个对象

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

If you want the constructor to always return the same object in memory, then you'll need to handle this yourself. 如果您希望构造函数始终在内存中返回相同的对象 ,则需要自己处理。 Maybe use some sort of factory: 也许使用某种工厂:

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

If, instead, you don't care about the constructor returning the same object, but you just care about the re-evaluation of k , you can just cache that part: 相反,如果您不关心构造函数返回相同的对象,而只关心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

The trivial answer is "this is what the language does according to the spec". 简单的答案是“这是语言根据规范所做的”。 That's the correct, but not very satisfying answer. 这是正确的,但不是很令人满意的答案。 It's more interesting why it does this. 为什么这样做会更有趣。

It might be clearer that it has to do this with a different example: 可能更清楚,它必须使用另一个示例来执行此操作:

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

When you're constructing two A 's, you can't know whether they are equal, because you haven't defined what it means for them to be equal (or what it means for B's to be equal). 当构造两个A ,您不知道它们是否相等,因为尚未定义它们相等意味着什么(或B相等意味着什么)。 And you can't statically intitialize k either, as it depends on the passed in B . 而且您也不能静态初始化k ,因为它取决于传入的B

If this has to print twice, it would be entirely intuitive if that would only be the case if k depends on b , but not if it doesn't depend on b . 如果必须打印两次,那么只有在k依赖于b的情况下才是完全直观的,而如果它不依赖于b话则不是那么直观。

When you ask 当你问

how should I implement this so that for all instances of a case class with same parameters, it should only execute a lazy val definition only once 我应该如何实现它,以便对于具有相同参数的case类的所有实例,只应执行一次惰性val定义一次

that's a trickier question than it sounds. 这是一个听起来棘手的问题。 You make "the same parameters" sound like something that can be known at compile time without further information. 您使“相同参数”听起来像在编译时就可以知道的东西,而无需进一步的信息。 It's not, you can only know it at runtime. 不是,您只能在运行时知道它。

And if you only know that at runtime, that means you have to keep all past uses of the instance A[B] alive. 而且,如果您仅在运行时知道这一点,则意味着您必须保留实例A[B]所有过去使用。 This is a built in memory leak - no wonder Scala has no built-in way to do this. 这是内置的内存泄漏-难怪Scala没有内置的方法可以做到这一点。

If you really want this - and think long and hard about the memory leak - construct a Map[B, A[B]] , and try to get a cached instance from that map, and if it doesn't exist, construct one and put it in the map. 如果您确实要这样做-并仔细考虑内存泄漏-构造一个Map[B, A[B]] ,然后尝试从该映射中获取一个缓存的实例,如果它不存在,则构造一个并把它放在地图上

I believe case class es only consider the arguments to their constructor (not any auxiliary constructor) to be part of their equality concept. 我相信case class仅将其构造函数(而不是任何辅助构造函数)的参数视为其相等性概念的一部分。 Consider when you use a case class in a match statement, unapply only gives you access (by default) to the constructor parameters. 想想当你在使用的情况下,类match的语句, unapply只给你访问(默认情况下)的构造函数的参数。

Consider anything in the body of case classes as "extra" or "side effect" stuffs. 将案例类主体中的任何内容视为“额外”或“副作用”。 I consider it a good tactic to make case classes as near-empty as possible and put any custom logic in a companion object. 我认为这是一个很好的策略,使案例类尽可能接近空,并将任何自定义逻辑放入伴随对象中。 Eg: 例如:

case class Foo(a:Int)

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

In addition to dhg answer, I should say, I'm not aware of functional language that does full constructor memoizing by default. 除了dhg答案外,我应该说,我不知道默认情况下不会进行完整构造函数记忆的功能语言。 You should understand that such memoizing means that all constructed instances should stick in memory, which is not always desirable. 您应该理解,这种记忆意味着所有构造的实例都应保留在内存中,这并不总是可取的。

Manual caching is not that hard, consider this simple code 手动缓存并不难,请考虑以下简单代码

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