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


248

Я заметил, что 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?

Ответы:


336

Разница между ними заключается в том, что a valвыполняется, когда он определен, тогда как a 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реализованы, посмотрите этот вопрос .



@PeterSchmitz И я нахожу это ужасным. Сравните с Lazy<T>.NET
Павел Воронин

61

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

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()

Но это приведет к тому же StackOverflowException, если ваш метод toString выведет атрибут «foo». Хороший пример "ленивый" в любом случае !!!
Фуад Эфенди

39

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

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', но не старое.

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


35

Ленивый вал легче всего понять как « запоминание (без аргументов)».

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

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

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

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


1
может быть, скорее как «записанное определение, которое принимает 0 аргументов».
Андрей Тюкин

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 

Это называется «ранний инициализатор». Посмотрите этот ТАК вопрос для более подробной информации.


11
Можете ли вы объяснить, почему объявление Y не сразу инициализирует переменную "x" в первом примере перед вызовом родительского конструктора?
Ashoat

2
Потому что конструктор суперкласса - первый, который вызывается неявно.
Стево Славич

@Ashoat Пожалуйста, смотрите эту ссылку для объяснения того, почему она не инициализирована.
Jus12

4

Демонстрация 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

1
scala> lazy val lazyEight = {
     |   println("I am lazy !")
     |   8
     | }
lazyEight: Int = <lazy>

scala> lazyEight
I am lazy !
res1: Int = 8
  • Все значения инициализируются при строительстве объекта.
  • Используйте ленивое ключевое слово, чтобы отложить инициализацию до первого использования
  • Внимание : Ленивые значения не являются окончательными и, следовательно, могут показывать недостатки производительности
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.