【问题标题】:Avoiding Scala memory leaks - Scala constructors避免 Scala 内存泄漏 - Scala 构造函数
【发布时间】:2009-08-02 13:09:07
【问题描述】:

我正在阅读“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中用上述算法构造一个有理数,而不会造成任何内存泄漏?

谢谢,

弗拉维乌·西普西根

【问题讨论】:

  • 谢谢,很抱歉再次提问:)。链接帖子中的答案澄清了我的问题。
  • 您确认denomnumer 真的是值吗?如果它们是 def denom = x$1._2 形式的“唯一”访问器方法,我一点也不感到惊讶。
  • 不是内存泄漏,是内存开销。

标签: scala memory-leaks


【解决方案1】:

伴随对象可以提供您需要的灵活性。它可以定义一个“静态”工厂方法来代替构造函数。

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

【讨论】:

  • 感谢您的回答,我也在考虑建立工厂,但这会增加一些复杂性。例如,用户可以调用对象的构造函数(例如 new Rational(10,20))并在此过程中创建无效的有理数。可以向构造函数添加 require(gcd(numerator, denominator) == 1) 或将类构造函数设为私有并强制用户使用工厂。我不确定什么是最好的……对于 Rational 来说,工厂似乎有点矫枉过正:)
  • 注意,由于工厂方法的名字是apply,所以可以这样调用:Rational(10, 20)
  • 不是矫枉过正——这是正确的答案。这对于 scala 来说是非常典型的,并且是推荐的模式 - 私有 ctor 并使用伴随应用。 :)
【解决方案2】:

你可以这样做:

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

}

【讨论】:

    【解决方案3】:

    你可以这样做:

    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)
    

    但这并不一定会让代码更容易理解。

    (注意:我实际上并没有尝试过,所以使用风险自负。)

    【讨论】:

    • 谢谢,您的第二个解决方案有效(尽管我没有进行任何严格的测试)并且确实以可忽略的开销摆脱了任何多余的字段。
    【解决方案4】:

    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) {
        // ...
    }
    

    【讨论】:

      【解决方案5】:

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

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

      我在这里遗漏了什么吗?

      【讨论】:

      • 问题是没有干净的方法来定义一个仅在对象构造期间使用的临时变量,就像使用 Java 构造函数一样。
      • 这不是正式的内存泄漏,因为它仍然可以从全局或局部变量访问(这就是 GC 不清理它的原因)。这肯定是一种非正式意义上的“泄漏”,即即使您永远不需要它,数据也会继续存在。
      • 我觉得标题的选择有点误导。
      【解决方案6】:

      我看到了这篇文章,您可能会觉得它很有用: 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
      }
      

      【讨论】:

        【解决方案7】:

        可能是这样的:

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

        而不是 val

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-07-30
          • 2014-11-30
          • 1970-01-01
          • 2018-04-08
          • 2013-06-24
          • 1970-01-01
          相关资源
          最近更新 更多