Getting zero with val depending on abstract def in attribute

I see some initialization confusion when mixing val and def in my dash. The situation can be summarized in the following example.

I have a trait that provides an abstract field, let's call it fruit , which should be implemented in child classes. It also uses this field in val:

 scala> class FruitTreeDescriptor(fruit: String) { | def describe = s"This tree has loads of ${fruit}s" | } defined class FruitTreeDescriptor scala> trait FruitTree { | def fruit: String | val descriptor = new FruitTreeDescriptor(fruit) | } defined trait FruitTree 

When overriding fruit with def everything works as expected:

 scala> object AppleTree extends FruitTree { | def fruit = "apple" | } defined object AppleTree scala> AppleTree.descriptor.describe res1: String = This tree has loads of apples 

However, if I override fruit with val ...

 scala> object BananaTree extends FruitTree { | val fruit = "banana" | } defined object BananaTree scala> BananaTree.descriptor.describe res2: String = This tree has loads of nulls 

What's going on here?

+6
source share
3 answers

Simply put, the moment you call:

 val descriptor = new FruitTreeDescriptor(fruit) 

the constructor for BananaTree has not yet been given the opportunity to run. This means that the value of fruit is still null , although it is val .

This is a subchannel of the famous vals non declarative initialization quirk , which can be illustrated with a simpler example:

 class A { val x = a val a = "String" } scala> new A().x res1: String = null 

(Although, fortunately, in this particular case, the compiler will detect something located and present a warning.)

To avoid the problem, declare fruit as lazy val , which will force a rating.

+3
source

The problem is the initialization order. val fruit = ... initialized after val descriptor = ... , so when descriptor initialized, fruit is still null . You can fix this by making fruit a lazy val , because then it will be initialized on first access.

+2
source

The descriptor field is initialized earlier than the fruit field, since the intializes property precedes the class, which extends it. null is the value of the field before initialization - this is why you get it. In the case of def this is just a method call instead of access to a certain field, therefore everything is fine (since the method code can be called several times - there is no initialization). See http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

Why is def so different? This is because def can be called several times, but val only once (therefore, its first and only call is actually fileld initialization).

A typical solution to this problem is instead, using lazy val , it will be initialized when you really need it. Another solution is the early integrators .

Another, simpler example of what happens:

 scala> class A {val a = b; val b = 5} <console>:7: warning: Reference to uninitialized value b class A {val a = b; val b = 5} ^ defined class A scala> (new A).a res2: Int = 0 //null 

More generally, scala can theoretically analyze the graph of dependencies between fields (which field needs another field) and begin initialization from the end nodes. But in practice, each module is compiled separately, and the compiler may not even know these dependencies (maybe even Java, which calls Scala, which calls Java), so it simply performs sequential initialization.

Thus, because of this, he could not even detect simple loops:

 scala> class A {val a: Int = b; val b: Int = a} <console>:7: warning: Reference to uninitialized value b class A {val a: Int = b; val b: Int = a} ^ defined class A scala> (new A).a res4: Int = 0 scala> class A {lazy val a: Int = b; lazy val b: Int = a} defined class A scala> (new A).a java.lang.StackOverflowError 

Actually, such a cycle (inside one module) can theoretically be detected in a separate assembly, but this will not help, since it is quite obvious.

+1
source

Source: https://habr.com/ru/post/985260/


All Articles