¿Qué hace un vago val?

248

Me di cuenta de que Scala proporciona lazy vals. Pero no entiendo lo que hacen.

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

El REPL muestra que yes un lazy val, pero ¿en qué se diferencia de un normal val?

kiritsuku
fuente

Respuestas:

335

La diferencia entre ellos es que a valse ejecuta cuando se define, mientras que a lazy valse ejecuta cuando se accede por primera vez.

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

A diferencia de un método (definido con def) a lazy valse ejecuta una vez y luego nunca más. Esto puede ser útil cuando una operación tarda mucho en completarse y cuando no está seguro si se usará más tarde.

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

Aquí, cuando los valores xy ynunca se usan, solo se xdesperdician recursos innecesariamente. Si suponemos que yno tiene efectos secundarios y que no sabemos con qué frecuencia se accede (nunca, una vez, miles de veces), es inútil declararlo defya que no queremos ejecutarlo varias veces.

Si desea saber cómo lazy valsse implementan, consulte esta pregunta .

kiritsuku
fuente
@PeterSchmitz Y esto me parece terrible. Comparar con Lazy<T>.NET
Pavel Voronin
61

Esta característica ayuda no solo a retrasar los costosos cálculos, sino que también es útil para construir estructuras mutuamente dependientes o cíclicas. Por ejemplo, esto lleva a un desbordamiento de pila:

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

Pero con vals perezosos funciona bien

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()
Landei
fuente
Pero conducirá a la misma StackOverflowException si su método toString genera el atributo "foo". Buen ejemplo de "perezoso" de todos modos!
Fuad Efendi
39

Entiendo que se da la respuesta, pero escribí un ejemplo simple para que sea fácil de entender para principiantes como yo:

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

La salida del código anterior es:

x
-----
y
y is: 18

Como se puede ver, x se imprime cuando se inicializa, pero y no se imprime cuando se inicializa de la misma manera (he tomado x como var intencionalmente aquí, para explicar cuándo se inicializa y). Luego, cuando se llama y, se inicializa y se tiene en cuenta el valor de la última 'x', pero no el anterior.

Espero que esto ayude.

Mital Pritmani
fuente
35

Un vago val se entiende más fácilmente como una " definición memorable (sin argumentos )".

Al igual que un def, un val vago no se evalúa hasta que se invoca. Pero el resultado se guarda para que las invocaciones posteriores devuelvan el valor guardado. El resultado memorizado ocupa espacio en su estructura de datos, como un val.

Como otros han mencionado, los casos de uso para un val perezoso son diferir cálculos costosos hasta que sean necesarios y almacenar sus resultados, y resolver ciertas dependencias circulares entre valores.

De hecho, los vals perezosos se implementan más o menos como defs memorables. Puede leer sobre los detalles de su implementación aquí:

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

tksfz
fuente
1
quizás más bien como una "definición memorable que toma 0 argumentos".
Andrey Tyukin
19

También lazyes útil sin dependencias cíclicas, como en el siguiente código:

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

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

El acceso Yahora arrojará una excepción de puntero nulo, porque xaún no se ha inicializado. Lo siguiente, sin embargo, funciona bien:

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

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

EDITAR: lo siguiente también funcionará:

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

Esto se llama un "inicializador temprano". Vea esta pregunta SO para más detalles.

Jus12
fuente
11
¿Puede aclarar por qué la declaración de Y no inicializa inmediatamente la variable "x" en el primer ejemplo antes de llamar al constructor principal?
Ashoat
2
Porque el constructor de superclase es el primero que se llama implícitamente.
Stevo Slavić
@Ashoat Consulte este enlace para obtener una explicación de por qué no se inicializa.
Jus12
4

Una demostración de lazy- como se definió anteriormente - ejecución cuando se define vs ejecución cuando se accede: (usando 2.12.7 scala shell)

// 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
pjames
fuente
1
scala> lazy val lazyEight = {
     |   println("I am lazy !")
     |   8
     | }
lazyEight: Int = <lazy>

scala> lazyEight
I am lazy !
res1: Int = 8
  • Todos los vals se inicializan durante la construcción del objeto.
  • Use la palabra clave perezosa para diferir la inicialización hasta el primer uso
  • Atención : los vals perezosos no son finales y, por lo tanto, pueden mostrar inconvenientes de rendimiento

fuente