Что делает ленивый вал?

214

Я заметил, что Scala предоставляет lazy vals. Но я не понимаю, что они делают.

scala> val x = 15
x: Int = 15

scala> lazy val y = 13
y: Int = <lazy>

scala> x
res0: Int = 15

scala> y
res1: Int = 13

REPL показывает, что y является lazy val, но как он отличается от обычного val?

Теги:
lazy-evaluation

7 ответов

310
Лучший ответ

Разница между ними заключается в том, что a val выполняется, когда он определен, тогда как lazy val выполняется при первом обращении к нему.

scala> val x = { println("x"); 15 }
x
x: Int = 15

scala> lazy val y = { println("y"); 13 }
y: Int = <lazy>

scala> x
res2: Int = 15

scala> y
y
res3: Int = 13

scala> y
res4: Int = 13

В отличие от метода (определенного с помощью def) a lazy val выполняется один раз, а затем никогда больше. Это может быть полезно, когда операция занимает много времени, и когда она не уверена, что она будет использоваться позже.

scala> class X { val x = { Thread.sleep(2000); 15 } }
defined class X

scala> class Y { lazy val y = { Thread.sleep(2000); 13 } }
defined class Y

scala> new X
res5: X = X@262505b7 // we have to wait two seconds to the result

scala> new Y
res6: Y = Y@1555bd22 // this appears immediately

Здесь, когда значения x и y никогда не используются, только x излишне тратит ресурсы. Если мы предположим, что y не имеет побочных эффектов и что мы не знаем, как часто он обращается (никогда, один раз, тысячи раз), бесполезно объявлять его как def, так как мы не хотим его выполнять несколько раз.

Если вы хотите знать, как реализованы lazy vals, см. этот question.

53

Эта функция помогает не только откладывать дорогостоящие вычисления, но также полезно создавать взаимно зависимые или циклические структуры. Например. это приводит к переполнению стека:

trait Foo { val foo: Foo }
case class Fee extends Foo { val foo = Faa() }
case class Faa extends Foo { val foo = Fee() }

println(Fee().foo)
//StackOverflowException

Но с ленивыми счетами он отлично работает

trait Foo { val foo: Foo }
case class Fee extends Foo { lazy val foo = Faa() }
case class Faa extends Foo { lazy val foo = Fee() }

println(Fee().foo)
//Faa()
  • 0
    Но это приведет к тому же StackOverflowException, если ваш метод toString выведет атрибут «foo». Хороший пример "ленивых" в любом случае !!!
34

Я понимаю, что ответ дан, но я написал простой пример, чтобы понять его для начинающих, таких как я:

var x = { println("x"); 15 }
lazy val y = { println("y"); x+1 }
println("-----")
x = 17
println("y is: " + y)

Вывод кода выше:

x
-----
y
y is: 18

Как можно видеть, x печатается, когда он инициализируется, но y не печатается, когда он инициализируется таким же образом (я принял x как var намеренно здесь - объяснить, когда y инициализируется). Далее, когда вызывается y, он инициализируется, а также значение последнего "x" принимается во внимание, но не является старым.

Надеюсь, что это поможет.

26

Ленивый val наиболее легко понимается как " memoized (no-arg) def".

Как def, lazy val не оценивается до его вызова. Но результат сохраняется, так что последующие вызовы возвращают сохраненное значение. Мембранный результат занимает место в вашей структуре данных, например, val.

Как отмечали другие, варианты использования для ленивого val - отложить дорогостоящие вычисления до тех пор, пока они не понадобятся, и сохранить их результаты, а также решить определенные круговые зависимости между значениями.

На самом деле ленивые валы реализованы более или менее как memoized defs. Вы можете прочитать о деталях их реализации здесь:

http://docs.scala-lang.org/sips/pending/improved-lazy-val-initialization.html

  • 1
    может быть, скорее как «записанное определение, которое принимает 0 аргументов».
  • 1
    @AndreyTyukin обновлен, чтобы отразить ваш комментарий
19

Также lazy полезен без циклических зависимостей, как в следующем коде:

abstract class X {
  val x: String
  println ("x is "+x.length)
}

object Y extends X { val x = "Hello" }
Y

Доступ к Y теперь приведет к исключению нулевого указателя, потому что x еще не инициализирован. Однако следующее работает отлично:

abstract class X {
  val x: String
  println ("x is "+x.length)
}

object Y extends X { lazy val x = "Hello" }
Y

ИЗМЕНИТЬ: будет также работать следующее:

object Y extends { val x = "Hello" } with X 

Это называется "ранним инициализатором". Подробнее см. этот вопрос SO.

  • 11
    Не могли бы вы пояснить, почему объявление Y не сразу инициализирует переменную "x" в первом примере перед вызовом родительского конструктора?
  • 2
    Потому что конструктор суперкласса - первый, который вызывается неявно.
Показать ещё 1 комментарий
1
scala> lazy val lazyEight = {
     |   println("I am lazy !")
     |   8
     | }
lazyEight: Int = <lazy>

scala> lazyEight
I am lazy !
res1: Int = 8
  • Все vals инициализируются при построении объекта
  • Использовать ленивое ключевое слово для отсрочки инициализации до первого использования
  • Внимание: Lazy vals не являются окончательными и поэтому могут показывать недостатки производительности.
0

Демонстрация lazy исполнения при определении и исполнении при доступе: (с использованием оболочки 2.12.7 scala)

// compiler says this is ok when it is lazy
scala> lazy val t: Int = t 
t: Int = <lazy>
//however when executed, t recursively calls itself, and causes a StackOverflowError
scala> t             
java.lang.StackOverflowError
...

// when the t is initialized to itself un-lazily, the compiler warns you of the recursive call
scala> val t: Int = t
<console>:12: warning: value t does nothing other than call itself recursively
   val t: Int = t

Ещё вопросы

Сообщество Overcoder
Наверх
Меню