Как избежать утечек памяти в Scala - конструкторы Scala

Я работал с книгой «Программирование на 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 с помощью вышеуказанного алгоритма, не вызывая утечек памяти?

Спасибо,

Флавиу Сипциган


person Flaviu Cipcigan    schedule 02.08.2009    source источник
comment
Тот же вопрос: stackoverflow.com/questions/1118669/   -  person Alexander Azarov    schedule 02.08.2009
comment
Спасибо и извините за вопрос снова :). Ответы в связанном сообщении прояснили мой вопрос.   -  person Flaviu Cipcigan    schedule 02.08.2009
comment
Вы подтвердили, что denom и numer действительно ценности? Я бы совсем не удивился, если бы это были только методы доступа формы def denom = x$1._2.   -  person Raphael    schedule 31.08.2011
comment
Это не утечка памяти, это накладные расходы на память.   -  person Daniel C. Sobral    schedule 31.08.2011


Ответы (7)


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

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)

Но это не обязательно делает код более понятным.

(Примечание: на самом деле я этого не пробовал, поэтому используйте на свой страх и риск.)

person jqno    schedule 02.08.2009
comment
Спасибо, ваше второе решение работает (хотя я не проводил тщательного тестирования) и избавляет от лишних полей с незначительными накладными расходами. - person Flaviu Cipcigan; 02.08.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 можно вычислить и использовать для получения двух значений конструктора.

person Thomas Jung    schedule 02.08.2009
comment
Спасибо за ответ, я тоже думал о фабрике, но это добавило бы пары сложностей. Например, пользователь может вызвать конструктор объекта (например, new Rational (10,20)) и в процессе создать недопустимое рациональное решение. Можно добавить в конструктор требование (gcd (числитель, знаменатель) == 1) или сделать конструктор класса закрытым и обязать пользователей использовать фабрику. Я не уверен, что было бы лучше ... фабрика действительно кажется излишней для Rational :) - person Flaviu Cipcigan; 02.08.2009
comment
Обратите внимание: поскольку имя фабричного метода apply, его можно вызвать так: Rational(10, 20). - person Alexey Romanov; 03.08.2009
comment
Не перебор - это правильный ответ. Это очень типично для scala и является рекомендуемым шаблоном - применить частный ctor и использовать сопутствующий элемент. :) - person Creos; 19.06.2019

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

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

}
person Virasak    schedule 20.09.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) {
    // ...
}
person Jesper    schedule 03.08.2009

... на самом деле, я не понимаю, как это представляет собой «утечку памяти».

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

Я что-то упустил?

person redz    schedule 03.11.2009
comment
Проблема в том, что нет чистого способа определить временную переменную, которая используется только во время создания объекта, как вы могли бы с конструктором Java. - person Cody Casterline; 28.08.2010
comment
Формально это не утечка памяти, поскольку она все еще доступна из глобальных или локальных переменных (вот почему GC не очищает ее). Это определенно утечка в неофициальном смысле, поскольку данные продолжают существовать, даже если они вам никогда не понадобятся. - person Malvolio; 10.01.2011
comment
Я думаю, что выбор названия немного вводит в заблуждение. - person ziggystar; 30.03.2011

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

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

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

вместо val

person Byambatsogt    schedule 29.03.2011