Co robi leniwa val?


248

Zauważyłem, że Scala zapewnia lazy vals. Ale nie rozumiem, co oni robią.

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

W rEPL pokazuje, że yjest lazy val, ale w jaki sposób różni się od normalnego val?

Odpowiedzi:


335

Różnica między nimi polega na tym, że a valjest wykonywane, gdy jest zdefiniowane, a a lazy valjest wykonywane, gdy jest uzyskiwane po raz pierwszy.

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

W przeciwieństwie do metody (zdefiniowanej za pomocą def) a lazy valjest wykonywane raz, a potem nigdy więcej. Może to być przydatne, gdy operacja zajmuje dużo czasu i nie ma pewności, czy zostanie później użyta.

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

Tutaj, gdy wartości xi ynigdy nie są używane, tylko xniepotrzebnie marnuje zasoby. Jeśli przypuszczamy, że ynie ma żadnych skutków ubocznych i nie wiemy, jak często jest on uzyskiwany (nigdy, raz, tysiące razy), nie ma sensu go deklarować, defponieważ nie chcemy go wykonywać kilka razy.

Jeśli chcesz wiedzieć, jak lazy valssą realizowane, zobacz to pytanie .



@PeterSchmitz I uważam to za okropne. Porównaj z Lazy<T>w .NET
Pavel Voronin

61

Ta funkcja pomaga nie tylko opóźniać kosztowne obliczenia, ale jest także przydatna do budowy wzajemnie zależnych lub cyklicznych struktur. Np. Prowadzi to do przepełnienia stosu:

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

Ale w przypadku leniwych vals działa dobrze

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

Doprowadzi to do tego samego wyjątku StackOverflowException, jeśli metoda toString wyświetli atrybut „foo”. W każdym razie fajny przykład „leniwego” !!!
Fuad Efendi,

39

Rozumiem, że odpowiedź jest podana, ale napisałem prosty przykład, aby ułatwić zrozumienie dla początkujących takich jak ja:

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

Wyjście powyższego kodu to:

x
-----
y
y is: 18

Jak widać, x jest drukowane podczas inicjalizacji, ale y nie jest drukowane, gdy jest inicjowany w ten sam sposób (wziąłem x jako var celowo tutaj - aby wyjaśnić, kiedy y jest inicjalizowany). Następnie, gdy wywoływane jest y, jest ono inicjalizowane, a także uwzględniana jest wartość ostatniego „x”, ale nie stara.

Mam nadzieję że to pomoże.


35

Leniwy val jest najłatwiejszy do zrozumienia jako „ zapamiętany (bez arg) def”.

Podobnie jak def, leniwa val nie jest oceniana, dopóki nie zostanie wywołana. Ale wynik jest zapisywany, więc kolejne wywołania zwracają zapisaną wartość. Zapamiętany wynik zajmuje miejsce w strukturze danych, podobnie jak val.

Jak wspomnieli inni, przypadki użycia leniwej wartości polegają na odkładaniu drogich obliczeń, aż będą potrzebne i na przechowywaniu ich wyników oraz na rozwiązaniu pewnych cyklicznych zależności między wartościami.

Leniwe vale są w rzeczywistości implementowane mniej więcej jako zapamiętane def. O szczegółach ich wdrożenia możesz przeczytać tutaj:

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


1
może raczej jako „zapamiętany def, który przyjmuje 0 argumentów”.
Andrey Tyukin,

19

lazyJest także przydatny bez cyklicznych zależności, jak w poniższym kodzie:

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

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

Uzyskiwanie dostępu Yspowoduje teraz zgłoszenie wyjątku zerowego wskaźnika, ponieważ xnie został jeszcze zainicjowany. Działa to jednak dobrze:

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

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

EDYCJA: działają również następujące elementy:

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

Nazywa się to „wczesnym inicjatorem”. Zobacz to SO pytanie, aby uzyskać więcej informacji.


11
Czy możesz wyjaśnić, dlaczego deklaracja Y nie inicjuje natychmiast zmiennej „x” w pierwszym przykładzie przed wywołaniem konstruktora nadrzędnego?
Ashoat

2
Ponieważ konstruktor nadklasy jest pierwszym, który jest domyślnie wywoływany.
Stevo Slavić

@Ashoat Proszę zobaczyć ten link, aby wyjaśnić, dlaczego nie został zainicjowany.
Jus12

4

Demonstracja lazy- jak zdefiniowano powyżej - wykonania po zdefiniowaniu vs wykonanie przy dostępie: (przy użyciu powłoki Scala 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
  • Wszystkie zawory są inicjowane podczas budowy obiektu
  • Użyj leniwego słowa kluczowego, aby odroczyć inicjalizację do pierwszego użycia
  • Uwaga : Leniwe wale nie są ostateczne i dlatego mogą wykazywać wady wydajności
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.