Що робить лінивий вал?


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?

Відповіді:


335

Різниця між ними полягає в тому, що 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реалізуються, перегляньте це питання .


65
Як доповнення: @ViktorKlang опублікував у Twitter: "Маловідомий факт Scala: якщо ініціалізація ледачого
Пітер Шмітц

@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, лінивий val не оцінюється, поки його не викликають. Але результат зберігається, щоб наступні виклики повернули збережене значення. Запам'ятовуваний результат займає місце у вашій структурі даних, як вал.

Як уже згадували інші, випадки використання для ледачого валу полягають у відкладанні дорогих обчислень, поки вони не знадобляться та зберігають їх результати, а також вирішують певні кругові залежності між величинами.

Ледачі вали насправді реалізуються більш-менш як запам'ятовані деф. Детальну інформацію про їх реалізацію можна прочитати тут:

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


1
може бути, скоріше, як "запам'ятована def, яка бере 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

EDIT: також буде працювати наступне:

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

Це називається "раннім ініціалізатором". Дивіться це питання ТА для отримання більш детальної інформації.


11
Чи можете ви пояснити, чому декларація Y не ініціалізує змінну "x" у першому прикладі перед тим, як викликати батьківський конструктор?
Ashoat

2
Тому що конструктор надкласового класу - це перший, який неявно називається.
Стево Славич

@Ashoat Будь ласка, перегляньте це посилання для пояснення того, чому воно не ініціалізовано.
липня

4

Демонстрація lazy- як визначено вище - виконання при визначенні проти виконання при зверненні: (використовуючи оболонку шкали 2.12.7)

// 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.