Как мне избежать непреднамеренного захвата локальной области видимости в функциональных литералах? - PullRequest
7 голосов
/ 18 октября 2010

Я попрошу об этом на примере Scala, но вполне возможно, что это влияет на другие языки, которые допускают гибридные императивные и функциональные стили.

Вот краткий пример ( ОБНОВЛЕНО , см. Ниже):

def method: Iterator[Int] {
    // construct some large intermediate value
    val huge = (1 to 1000000).toList        
    val small = List.fill(5)(scala.util.Random.nextInt)
    // accidentally use huge in a literal
    small.iterator filterNot ( huge contains _ )    
}

Теперь iterator.filterNot работает лениво, и это здорово! В результате мы ожидаем, что возвращенный итератор не будет занимать много памяти (в действительности, O (1)). К сожалению, однако, мы допустили ужасную ошибку: поскольку filterNot ленив, он сохраняет ссылку на литерал функции huge contains _.

Таким образом, хотя мы думали, что метод потребует большого объема памяти во время его работы, и что эта память может быть освобождена сразу после завершения метода, на самом деле эта память застревает до тех пор, пока мы не забудем возвращенную Iterator.

(Я только что совершил такую ​​ошибку, на поиски которой ушло много времени! Вы можете поймать такие вещи, глядя на свалки в кучу ...)

Каков наилучший способ избежать этой проблемы?

Кажется, что единственное решение - тщательно проверить функциональные литералы, которые переживают конец области видимости и которые захватили промежуточные переменные. Это немного неловко, если вы создаете нестрогую коллекцию и планируете ее вернуть. Кто-нибудь может придумать какие-нибудь хорошие трюки, специфичные для Scala или иные, которые помогут избежать этой проблемы и позволят мне написать хороший код?

ОБНОВЛЕНИЕ: пример, который я привел ранее, был глуп, как демонстрирует приведенный ниже ответ huynhjl. Это было:

def method: Iterator[Int] {
    val huge = (1 to 1000000).toList // construct some large intermediate value
    val n = huge.last                // do some calculation based on it
    (1 to n).iterator map (_ + 1)    // return some small value 
}

На самом деле, теперь, когда я немного лучше понимаю, как все это работает, я не так волнуюсь!

1 Ответ

5 голосов
/ 18 октября 2010

Вы уверены, что не упрощаете контрольный пример?Вот что я запускаю:

object Clos {
  def method: Iterator[Int] = {
    val huge = (1 to 2000000).toList
    val n = huge.last
    (1 to n).iterator map (_ + 1)
  }

  def gc() { println("GC!!"); Runtime.getRuntime.gc }

  def main(args:Array[String]) {
    val list = List(method, method, method)
    list.foreach(m => println(m.next))
    gc()
    list.foreach(m => println(m.next))
    list.foreach(m => println(m.next))
  }
}

Если я вас правильно понимаю, потому что main использует итераторы даже после вызова gc(), JVM будет удерживать объекты huge.

Вот так я его запускаю:

JAVA_OPTS="-verbose:gc" scala -cp classes Clos

Вот что он печатает к концу:

[Full GC 57077K->57077K(60916K), 0.3340941 secs]
[Full GC 60852K->60851K(65088K), 0.3653304 secs]
2
2
2
GC!!
[Full GC 62959K->247K(65088K), 0.0610994 secs]
3
3
3
4
4
4

Так что мне кажется, что hugeобъекты были восстановлены ...

...