Как избежать утечек памяти в Scala - конструкторы Scala - PullRequest
9 голосов
/ 02 августа 2009

Я работал над книгой «Программирование в Scala», и меня поразила небольшая проблема с реализацией класса Rational в главе 6.

Это моя первоначальная версия класса 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 ]

13 голосов
/ 20 сентября 2009

Вы можете сделать это:

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 голосов
/ 02 августа 2009

Сопутствующий объект может обеспечить необходимую гибкость. Он может определять «статические» фабричные методы, которые заменяют конструктор.

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 можно вычислить и использовать для получения двух значений конструктора.

6 голосов
/ 03 августа 2009

Есть небольшая проблема с примером Томаса Юнга; он по-прежнему позволяет вам создать объект Rational с общим термином в числителе и знаменателе - если вы создаете объект Rational самостоятельно, используя 'new' вместо объекта-компаньона:

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

Этого можно избежать, потребовав от клиентского кода всегда использовать объект-компаньон для создания объекта Rational, сделав неявный конструктор приватным:

class Rational private (numerator: Int, denominator: Int) {
    // ...
}
6 голосов
/ 02 августа 2009

Вы можете сделать это:

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 голосов
/ 04 ноября 2009

... на самом деле, я не вижу, как это является "утечкой памяти".

Вы объявляете последнее поле в пределах экземпляра класса, а затем, очевидно, удивляетесь, что оно "висит" Какое поведение вы ожидали?

Я что-то здесь упускаю?

0 голосов
/ 29 марта 2011

Может быть как:

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

вместо val

0 голосов
/ 29 августа 2010

Я наткнулся на эту статью, которая может оказаться вам полезной: http://daily -scala.blogspot.com / 2010/02 / временные-переменные-во-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
}
...