简体   繁体   中英

A deeper explanation of why Lazy Vals work in scala constructors?

I understand the general use of Lazy vals to get around initialization order problems in scala, but something has always bothered me about this explanation. If a "Lazy Val" is initialized during it's first access, and the parent constructor is making use of it BEFORE it could possibly exist - what exactly is going on here? In the below example, when "println("A: " + x1)" is called - Class B doesn't exist yet.. but the value does correctly print. At the exact moment we see "A: Hello" - did this happen in the constructor of A, or delayed somehow until B fully existed? In a sense, marking it "Lazy" has counter-intuitively made it available ahead of schedule?

Thank you

(referenced from https://github.com/paulp/scala-faq/wiki/Initialization-Order )

abstract class A {
  val x1: String

  println("A: " + x1)
}
class B extends A {
  lazy val x1: String = "hello"    

}

The object itself doesn't exist, but fields within the object can exist and be calculated.

What's happening is that from within A's constructor, it's accessing x1 and therefore forcing the lazy value to be computed. The reason A can know it needs to call B's x1 method, is because it's dynamically dispatched (just like in Java).

If it helps, the stack would be something similar to this:

B.x1$lzycompute
B.x1
A.<init>
B.<init>

If it helps, here is a rough version of your code in Java:

public class Testing {

    public static void main(String[] args) {
        new B();
    }

    public static abstract class A {

        public abstract String x1();

        public A() {
            System.out.println(x1());
        }
    }

    public static class B extends A {
        private boolean inited = false;
        private String x1;

        private String computeX1() {
            x1 = "hello";
            inited = true;
            return x1;
        }

        public String x1() {
            return this.inited ? x1 : computeX1();
        }
    }

}

The "BEFORE" relation just refers to the order that initializers are run.

When you allocate an object on the heap, you simply allocate it and then call the init methods to init it.

There's no sense in which there is an instance of parent A that precedes an instance of child B.

They are the same object, seen as the parts of its type.

It's not like when people tell me I look like my father (who is not me).

Anyway, fragility ensues if it's not laziness all the way down:

abstract class A {
  val x1: String
  val x2: String
  println("A: " + x1)
  println("A2: " + x2)
}
class B extends A {
  lazy val x1: String = "hello"
  lazy val x2: String = x3
  val x3: String = "bye"
}
object Test extends App {
  val b = new B
  Console println (b.x1,b.x2,b.x3)
}

With the result:

A: hello
A2: null
(hello,null,bye)

That's why the general advice is to use defs instead of vals and, for that matter, traits instead of classes ( to ensure since with traits you are more likely to have heard of and followed the first rule).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM