วาลขี้เกียจทำอะไร?


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

ความแตกต่างระหว่างพวกเขาคือที่valจะดำเนินการเมื่อมีการกำหนดในขณะที่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
Pavel Voronin

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

แต่ด้วย vals ขี้เกียจมันใช้งานได้ดี

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" ตัวอย่างที่ดีของ "ขี้เกียจ" อยู่ดี !!!
Fuad Efendi

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 ( memoized (no-ARG))"

เช่น def, ขี้เกียจ val จะไม่ถูกประเมินจนกว่าจะถูกเรียกใช้ แต่ผลลัพธ์จะถูกบันทึกไว้เพื่อให้การเรียกใช้ครั้งต่อมาส่งคืนค่าที่บันทึกไว้ ผลลัพธ์ที่บันทึกไว้นั้นใช้พื้นที่ในโครงสร้างข้อมูลของคุณเช่นวาล

ดังที่คนอื่น ๆ ได้กล่าวถึงกรณีการใช้งานสำหรับขี้เกียจวาลจะชะลอการคำนวณราคาแพงจนกว่าพวกเขาจะมีความจำเป็นและเก็บผลลัพธ์ของพวกเขาและเพื่อแก้ปัญหาการพึ่งพาวงกลมระหว่างค่าบางอย่าง

อันที่จริงแล้ว Lazy vals นั้นถูกนำไปใช้งานไม่มากก็น้อยตามที่บันทึกไว้ใน defs คุณสามารถอ่านรายละเอียดการใช้งานได้ที่นี่:

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


1
อาจจะเป็น "def memoized ที่ใช้เวลา 0 ข้อโต้แย้ง"
Andrey Tyukin

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 

สิ่งนี้เรียกว่า "Early initializer" ดูคำถาม SO นี้สำหรับรายละเอียดเพิ่มเติม


11
คุณสามารถอธิบายได้ไหมว่าทำไมการประกาศของ Y ไม่ได้เริ่มต้นตัวแปร "x" ในตัวอย่างแรกก่อนที่จะเรียก parent constructor?
Ashoat

2
เพราะนวกรรมิก superclass เป็นคนแรกที่ได้รับการเรียกโดยปริยาย
Stevo Slavić

@Ashoat โปรดดูลิงค์นี้สำหรับคำอธิบายว่าทำไมจึงไม่เริ่มต้น
Jus12

4

การสาธิตของlazy- ตามที่กำหนดไว้ข้างต้น - การดำเนินการเมื่อกำหนดกับการดำเนินการเมื่อเข้าถึง: (ใช้ 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

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

scala> lazyEight
I am lazy !
res1: Int = 8
  • vals ทั้งหมดถูกเตรียมใช้งานระหว่างการสร้างวัตถุ
  • ใช้คำหลักขี้เกียจเพื่อเลื่อนการเริ่มต้นจนกระทั่งการใช้งานครั้งแรก
  • ข้อควรระวัง : VAL ของ Lazy ไม่ถือเป็นที่สุดดังนั้นอาจแสดงข้อเสียประสิทธิภาพ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.