O que um val preguiçoso faz?

248

Notei que Scala fornece lazy vals. Mas eu não entendo o que eles fazem.

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

O REPL mostra que yé um lazy val, mas como é diferente de um normal val?

kiritsuku
fonte

Respostas:

335

A diferença entre eles é que a valé executado quando definido, enquanto a lazy valé executado quando acessado pela primeira 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

Ao contrário de um método (definido com def), a lazy valé executado uma vez e nunca mais. Isso pode ser útil quando uma operação leva muito tempo para ser concluída e quando não se tem certeza se será usada posteriormente.

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

Aqui, quando os valores xe ynunca são usados, apenas xdesperdiçando recursos desnecessariamente. Se supusermos que yisso não tem efeitos colaterais e que não sabemos com que frequência ele é acessado (nunca, uma vez, milhares de vezes), é inútil declará-lo, defpois não queremos executá-lo várias vezes.

Se você quiser saber como lazy valssão implementadas, consulte esta pergunta .

kiritsuku
fonte
@ PeterSchmitz E acho isso terrível. Compare com Lazy<T>.NET
Pavel Voronin
61

Esse recurso ajuda não apenas a atrasar cálculos caros, mas também é útil para construir estruturas dependentes ou cíclicas mútuas. Por exemplo, isso leva a um estouro de pilha:

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

Mas com vals preguiçosos, funciona bem

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
fonte
Mas isso levará à mesma StackOverflowException se o método toString gerar o atributo "foo". Bom exemplo de "preguiçoso" de qualquer maneira !!!
Fuad Efendi
39

Entendo que a resposta foi dada, mas escrevi um exemplo simples para facilitar o entendimento de iniciantes como eu:

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

A saída do código acima é:

x
-----
y
y is: 18

Como pode ser visto, x é impresso quando inicializado, mas y não é impresso quando inicializado da mesma maneira (tomei x como var intencionalmente aqui - para explicar quando y é inicializado). Em seguida, quando y é chamado, é inicializado, assim como o valor do último 'x' é levado em consideração, mas não o antigo.

Espero que isto ajude.

Mital Pritmani
fonte
35

Um val preguiçoso é mais facilmente entendido como um " def memorizado (sem argumento)".

Como um def, um val preguiçoso não é avaliado até ser invocado. Mas o resultado é salvo para que as chamadas subseqüentes retornem o valor salvo. O resultado memorizado ocupa espaço em sua estrutura de dados, como um valor.

Como outros já mencionaram, os casos de uso de um val lento são adiar cálculos caros até que sejam necessários e armazenar seus resultados e resolver certas dependências circulares entre valores.

Vals preguiçosos são de fato implementados mais ou menos como padrões memorizados. Você pode ler sobre os detalhes de sua implementação aqui:

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

tksfz
fonte
1
talvez mais como um "def memorizado que recebe 0 argumentos".
Andrey Tyukin 28/09
19

Também lazyé útil sem dependências cíclicas, como no código a seguir:

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

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

O acesso Yagora lançará uma exceção de ponteiro nulo, porque xainda não foi inicializado. O seguinte, no entanto, funciona bem:

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

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

EDIT: o seguinte também funcionará:

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

Isso é chamado de "inicializador inicial". Veja esta pergunta para mais detalhes.

Jus12
fonte
11
Você pode esclarecer por que a declaração de Y não inicializa imediatamente a variável "x" no primeiro exemplo antes de chamar o construtor pai?
Ashoat 26/03
2
Porque o construtor da superclasse é o primeiro a ser chamado implicitamente.
Stevo Slavić
@ Ashoat Consulte este link para obter uma explicação de por que não foi inicializado.
precisa saber é o seguinte
4

Uma demonstração de lazy- como definido acima - execução quando definida vs execução quando acessada: (usando o shell 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
pjames
fonte
1
scala> lazy val lazyEight = {
     |   println("I am lazy !")
     |   8
     | }
lazyEight: Int = <lazy>

scala> lazyEight
I am lazy !
res1: Int = 8
  • Todos os valores são inicializados durante a construção do objeto
  • Use a palavra-chave lenta para adiar a inicialização até o primeiro uso
  • Atenção : Vals preguiçosos não são finais e, portanto, podem mostrar desvantagens de desempenho

fonte