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

时间:2009-08-02 13:09:08

标签: scala memory-leaks

我正在阅读“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

7 个答案:

答案 0 :(得分: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

}

答案 1 :(得分:12)

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

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可以计算并用于导出两个构造函数值。

答案 2 :(得分: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)

但它并不一定使代码更容易理解。

(注意:我实际上没有尝试过,所以请自担风险。)

答案 3 :(得分:6)

Thomas Jung的例子有一个小问题;它仍允许您创建一个在分子和分母中具有公共术语的Rational对象 - 如果您自己使用'new'而不是通过伴随对象创建Rational对象:

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

您可以通过要求客户端代码始终使用伴随对象来创建Rational对象来避免这种情况,方法是将隐式构造函数设为私有:

class Rational private (numerator: Int, denominator: Int) {
    // ...
}

答案 4 :(得分:3)

......实际上,我不知道这是如何构成“内存泄漏”的。

您在类实例的范围内声明了一个final字段,然后显然对它“挂起”感到惊讶。你期待什么行为?

我在这里遗漏了什么吗?

答案 5 :(得分: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
}

答案 6 :(得分:0)

可能像:

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

而不是val