避免Scala内存泄漏 - Scala构造函数

9

我正在阅读《Scala编程》这本书,但在第6章实现Rational类时遇到了一些问题。

这是我根据书中内容编写的Rational类的初始版本。

class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  private val g = gcd(numerator.abs, denominator.abs)

  val numer = numerator / g
  val denom = denominator / g

  override def toString  = numer + "/" + denom

  private def gcd(a: Int, b: Int): Int =
    if(b == 0) a else gcd(b, a % b)

  // other methods go here, neither access g
}

这里的问题是,即使再也没有访问过,字段g仍然存在于类的生命周期中。可以通过运行以下模拟程序来看到这个问题:

object Test extends Application {

  val a = new Rational(1, 2)
  val fields = a.getClass.getDeclaredFields

  for(field <- fields) {
    println("Field name: " + field.getName)
    field.setAccessible(true)
    println(field.get(a) + "\n")
  }  

}

它的输出将会是:
Field: denom
2

Field: numer
1

Field: g
1

我在Scala Wiki上找到了一种解决方案,具体如下:
class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  val (numer, denom) = { 
    val g = gcd(numerator.abs, denominator.abs)
    (numerator / g, denominator / g)
  }

  override def toString  = numer + "/" + denom

  private def gcd(a: Int, b: Int): Int =
    if(b == 0) a else gcd(b, a % b)

  // other methods go here
}

在这里,字段g仅局限于其块,但是,在运行小型测试应用程序时,我发现另一个字段x$1保留了包含(numer,denom)的元组的副本!

Field: denom
2

Field: numer
1

Field: x$1
(1,2)

有没有办法使用上述算法在Scala中构建一个有理数,而不会导致任何内存泄漏?

谢谢,

Flaviu Cipcigan


1
同样的问题:https://dev59.com/r3NA5IYBdhLWcg3wBpHs - Alexander Azarov
谢谢,再次提问很抱歉:)。链接帖子中的答案澄清了我的问题。 - Flaviu Cipcigan
你确认了 denomnumer 是真正的值吗?如果它们只是形如 def denom = x$1._2 的访问器方法,我一点也不会感到惊讶。 - Raphael
2
这不是内存泄漏,而是内存开销。 - Daniel C. Sobral
7个回答

13

你可以这样做:

object Rational {
    def gcd(a: Int, b: Int): Int =
        if(b == 0) a else gcd(b, a % b)
}

class Rational private (n: Int, d: Int, g: Int) {
    require(d != 0)

    def this(n: Int, d: Int) = this(n, d, Rational.gcd(n.abs, d.abs))

    val numer = n / g

    val denom = d / g

    override def toString = numer + "/" + denom

}

13

伴生对象可以提供您所需的灵活性。它可以定义“静态”工厂方法来替换构造函数。

object Rational{

    def apply(numerator: Int, denominator: Int) = {
        def gcd(a: Int, b: Int): Int = if(b == 0) a else gcd(b, a % b)
        val g = gcd(numerator, denominator)
        new Rational(numerator / g, denominator / g)
    }
}

class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  override def toString  = numerator + "/" + denominator
  // other methods go here, neither access g
}

val r = Rational(10,200)

在工厂方法的范围内,g 可以被计算并用于推导出两个构造函数的值。


1
谢谢您的回答,我也考虑过工厂模式,但这会增加一些复杂性。例如,用户可能会调用对象的构造函数(如new Rational(10,20)),在此过程中创建无效的有理数。可以将require(gcd(numerator,denominator)== 1)添加到构造函数中,或将类构造函数设置为private并强制用户使用工厂方法。我不确定哪种方法最好...对于 Rational 来说,工厂模式似乎有点过头了 :) - Flaviu Cipcigan
2
请注意,由于工厂方法的名称是“apply”,因此可以像这样调用它:“Rational(10, 20)”。 - Alexey Romanov
不是过度设计——这是正确的答案。这在 Scala 中非常典型,也是推荐的模式——私有构造函数并使用伴生对象的 apply 方法。 :) - Creos

6

Thomas Jung的例子有一个小问题,它仍然允许你创建一个分子和分母中都有公共项的有理数对象——如果你自己使用“new”创建有理数对象,而不是通过伴生对象创建:

val r = new Rational(10, 200) // Oops! Creating a Rational with a common term

您可以通过要求客户端代码始终使用伴生对象来创建有理数对象,或者将隐式构造函数设为私有来避免出现这种情况:
class Rational private (numerator: Int, denominator: Int) {
    // ...
}

6
您可以这样做:
val numer = numerator / gcd(numerator.abs, denominator.abs)
val denom = denominator / gcd(numerator.abs, denominator.abs)

当然,你需要进行两次计算。但是,优化通常是内存/空间和执行时间之间的权衡。也许还有其他方法,但程序可能会变得过于复杂,如果有一个地方优化很少是过早的,那就是大脑功率优化 : )。例如,你可以这样做:
val numer = numerator / gcd(numerator.abs, denominator.abs)
val denom = denominator / (numerator / numer)

但这并不一定使代码更易懂。
(注意:我实际上没有尝试过这个,所以使用时请自行决定风险。)

谢谢,你的第二个解决方案可行(虽然我还没有进行严格测试),并且可以消除任何不必要的字段,开销很小。 - Flaviu Cipcigan

3

...实际上,我不明白这如何构成“内存泄漏”。

您在类实例的范围内声明一个final字段,然后显然惊讶于它“挂起来了”。您期望出现什么行为?

我是否漏掉了什么?


3
问题在于没有一种清晰的方式来定义一个仅在对象构造期间使用的临时变量,就像在Java构造函数中那样。 - Cody Casterline
3
这并不是一个正式的内存泄漏,因为它仍然可以从全局或局部变量中访问到(这就是为什么垃圾回收器不会清理它的原因)。但从非正式意义上来说,它肯定是一种"泄漏",因为这些数据即使你永远不需要它也仍然存在。 - Michael Lorton
我认为标题的选择有点误导。 - ziggystar

0

可以是这样:

def g = gcd(numerator.abs, denominator.abs)

使用“val”代替


0
我发现了一篇你可能会觉得有用的文章: http://daily-scala.blogspot.com/2010/02/temporary-variables-during-object.html 看起来你可以这样写:
class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  val (numer,denom) = {
      val g = gcd(numerator.abs, denominator.abs)
      (numerator/g, denominator/g)
  }

  override def toString  = numer + "/" + denom

  private def gcd(a: Int, b: Int): Int =
    if(b == 0) a else gcd(b, a % b)

  // other methods go here, neither access g
}

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