Skip to content

Commit 46a8ffe

Browse files
authored
Merge pull request #3017 from lrytz/faq-init
Update FAQ on initialization order
2 parents 4cdb87a + 19967a9 commit 46a8ffe

File tree

1 file changed

+103
-87
lines changed

1 file changed

+103
-87
lines changed

_overviews/FAQ/initialization-order.md

+103-87
Original file line numberDiff line numberDiff line change
@@ -7,158 +7,174 @@ permalink: /tutorials/FAQ/:title.html
77

88
## Example
99

10-
To understand the problem, let's pick the following concrete example.
10+
The following example illustrates how classes in a subclass relation
11+
witness the initialization of two fields which are inherited from
12+
their top-most parent. The values are printed during the constructor
13+
of each class, that is, when an instance is initialized.
1114

1215
abstract class A {
1316
val x1: String
1417
val x2: String = "mom"
1518

16-
println("A: " + x1 + ", " + x2)
19+
println(s"A: $x1, $x2")
1720
}
1821
class B extends A {
1922
val x1: String = "hello"
2023

21-
println("B: " + x1 + ", " + x2)
24+
println(s"B: $x1, $x2")
2225
}
2326
class C extends B {
2427
override val x2: String = "dad"
2528

26-
println("C: " + x1 + ", " + x2)
29+
println(s"C: $x1, $x2")
2730
}
2831

29-
Let's observe the initialization order through the Scala REPL:
32+
In the Scala REPL we observe:
3033

3134
scala> new C
3235
A: null, null
3336
B: hello, null
3437
C: hello, dad
3538

36-
Only when we get to the constructor of `C` are both `x1` and `x2` initialized. Therefore, constructors of `A` and `B` risk running into `NullPointerException`s.
39+
Only when we get to the constructor of `C` are both `x1` and `x2` properly initialized.
40+
Therefore, constructors of `A` and `B` risk running into `NullPointerException`s,
41+
since fields are null-valued until set by a constructor.
3742

3843
## Explanation
39-
A 'strict' or 'eager' val is one which is not marked lazy.
4044

41-
In the absence of "early definitions" (see below), initialization of strict vals is done in the following order.
45+
A "strict" or "eager" val is a `val` which is not a `lazy val`.
46+
Initialization of strict vals is done in the following order:
4247

4348
1. Superclasses are fully initialized before subclasses.
44-
2. Otherwise, in declaration order.
45-
46-
Naturally when a val is overridden, it is not initialized more than once. So though x2 in the above example is seemingly defined at every point, this is not the case: an overridden val will appear to be null during the construction of superclasses, as will an abstract val.
47-
48-
There is a compiler flag which can be useful for identifying this situation:
49-
50-
**-Xcheckinit**: Add runtime check to field accessors.
51-
52-
It is inadvisable to use this flag outside of testing. It adds significantly to the code size by putting a wrapper around all potentially uninitialized field accesses: the wrapper will throw an exception rather than allow a null (or 0/false in the case of primitive types) to silently appear. Note also that this adds a *runtime* check: it can only tell you anything about code paths which you exercise with it in place.
53-
54-
Using it on the opening example:
55-
56-
% scalac -Xcheckinit a.scala
57-
% scala -e 'new C'
58-
scala.UninitializedFieldError: Uninitialized field: a.scala: 13
59-
at C.x2(a.scala:13)
60-
at A.<init>(a.scala:5)
61-
at B.<init>(a.scala:7)
62-
at C.<init>(a.scala:12)
63-
64-
### Solutions ###
49+
2. Within the body or "template" of a class, vals are initialized in declaration order,
50+
the order in which they are written in source.
51+
52+
When a `val` is overridden, it's more precise to say that its accessor method (the "getter") is overridden.
53+
So the access to `x2` in class `A` invokes the overridden getter in class `C`.
54+
That getter reads the underlying field `C.x2`.
55+
This field is not yet initialized during the construction of `A`.
56+
57+
## Mitigation
58+
59+
The [`-Wsafe-init` compiler flag](https://docs.scala-lang.org/scala3/reference/other-new-features/safe-initialization.html)
60+
in Scala 3 enables a compile-time warning for accesses to uninitialized fields:
61+
62+
-- Warning: Test.scala:8:6 -----------------------------------------------------
63+
8 | val x1: String = "hello"
64+
| ^
65+
| Access non-initialized value x1. Calling trace:
66+
| ├── class B extends A { [ Test.scala:7 ]
67+
| │ ^
68+
| ├── abstract class A { [ Test.scala:1 ]
69+
| │ ^
70+
| └── println(s"A: $x1, $x2") [ Test.scala:5 ]
71+
| ^^
72+
73+
In Scala 2, the `-Xcheckinit` flag adds runtime checks in the generated bytecode to identify accesses of uninitialized fields.
74+
That code throws an exception when an uninitialized field is referenced
75+
that would otherwise be used as a `null` value (or `0` or `false` in the case of primitive types).
76+
Note that these runtime checks only report code that is actually executed at runtime.
77+
Although these checks can be helpful to find accesses to uninitialized fields during development,
78+
it is never advisable to enable them in production code due to the performance cost.
79+
80+
## Solutions
6581

6682
Approaches for avoiding null values include:
6783

68-
#### Use lazy vals ####
69-
70-
abstract class A {
71-
val x1: String
72-
lazy val x2: String = "mom"
84+
### Use class / trait parameters
7385

86+
abstract class A(val x1: String, val x2: String = "mom") {
7487
println("A: " + x1 + ", " + x2)
7588
}
76-
class B extends A {
77-
lazy val x1: String = "hello"
78-
89+
class B(x1: String = "hello", x2: String = "mom") extends A(x1, x2) {
7990
println("B: " + x1 + ", " + x2)
8091
}
81-
class C extends B {
82-
override lazy val x2: String = "dad"
83-
92+
class C(x2: String = "dad") extends B(x2 = x2) {
8493
println("C: " + x1 + ", " + x2)
8594
}
8695
// scala> new C
8796
// A: hello, dad
8897
// B: hello, dad
8998
// C: hello, dad
9099

91-
Usually the best answer. Unfortunately you cannot declare an abstract lazy val. If that is what you're after, your options include:
100+
Values passed as parameters to the superclass constructor are available in its body.
92101

93-
1. Declare an abstract strict val, and hope subclasses will implement it as a lazy val or with an early definition. If they do not, it will appear to be uninitialized at some points during construction.
94-
2. Declare an abstract def, and hope subclasses will implement it as a lazy val. If they do not, it will be re-evaluated on every access.
95-
3. Declare a concrete lazy val which throws an exception, and hope subclasses override it. If they do not, it will... throw an exception.
102+
Scala 3 also [supports trait parameters](https://docs.scala-lang.org/scala3/reference/other-new-features/trait-parameters.html).
96103

97-
An exception during initialization of a lazy val will cause the right-hand side to be re-evaluated on the next access: see SLS 5.2.
104+
Note that overriding a `val` class parameter is deprecated / disallowed in Scala 3.
105+
Doing so in Scala 2 can lead to surprising behavior.
98106

99-
Note that using multiple lazy vals creates a new risk: cycles among lazy vals can result in a stack overflow on first access.
107+
### Use lazy vals
100108

101-
#### Use early definitions ####
102109
abstract class A {
103-
val x1: String
104-
val x2: String = "mom"
110+
lazy val x1: String
111+
lazy val x2: String = "mom"
105112

106113
println("A: " + x1 + ", " + x2)
107114
}
108-
class B extends {
109-
val x1: String = "hello"
110-
} with A {
115+
class B extends A {
116+
lazy val x1: String = "hello"
117+
111118
println("B: " + x1 + ", " + x2)
112119
}
113-
class C extends {
114-
override val x2: String = "dad"
115-
} with B {
120+
class C extends B {
121+
override lazy val x2: String = "dad"
122+
116123
println("C: " + x1 + ", " + x2)
117124
}
118125
// scala> new C
119126
// A: hello, dad
120127
// B: hello, dad
121128
// C: hello, dad
122129

123-
Early definitions are a bit unwieldy, there are limitations as to what can appear and what can be referenced in an early definitions block, and they don't compose as well as lazy vals: but if a lazy val is undesirable, they present another option. They are specified in SLS 5.1.6.
130+
Note that abstract `lazy val`s are supported in Scala 3, but not in Scala 2.
131+
In Scala 2, you can define an abstract `val` or `def` instead.
124132

125-
Note that early definitions are deprecated in Scala 2.13; they will be replaced by trait parameters in Scala 3. So, early definitions are not recommended for use if future compatibility is a concern.
133+
An exception during initialization of a lazy val will cause the right-hand side to be re-evaluated on the next access; see SLS 5.2.
126134

127-
#### Use constant value definitions ####
128-
abstract class A {
129-
val x1: String
130-
val x2: String = "mom"
135+
Note that using multiple lazy vals incurs a new risk: cycles among lazy vals can result in a stack overflow on first access.
136+
When lazy vals are annotated as thread-safe in Scala 3, they risk deadlock.
131137

132-
println("A: " + x1 + ", " + x2)
133-
}
134-
class B extends A {
135-
val x1: String = "hello"
136-
final val x3 = "goodbye"
138+
### Use a nested object
137139

138-
println("B: " + x1 + ", " + x2)
139-
}
140-
class C extends B {
141-
override val x2: String = "dad"
140+
For purposes of initialization, an object that is not top-level is the same as a lazy val.
142141

143-
println("C: " + x1 + ", " + x2)
142+
There may be reasons to prefer a lazy val, for example to specify the type of an implicit value,
143+
or an object where it is a companion to a class. Otherwise, the most convenient syntax may be preferred.
144+
145+
As an example, uninitialized state in a subclass may be accessed during construction of a superclass:
146+
147+
class Adder {
148+
var sum = 0
149+
def add(x: Int): Unit = sum += x
150+
add(1) // in LogAdder, the `added` set is not initialized yet
151+
}
152+
class LogAdder extends Adder {
153+
private var added: Set[Int] = Set.empty
154+
override def add(x: Int): Unit = { added += x; super.add(x) }
144155
}
145-
abstract class D {
146-
val c: C
147-
val x3 = c.x3 // no exceptions!
148-
println("D: " + c + " but " + x3)
156+
157+
In this case, the state can be initialized on demand by wrapping it in a local object:
158+
159+
class Adder {
160+
var sum = 0
161+
def add(x: Int): Unit = sum += x
162+
add(1)
149163
}
150-
class E extends D {
151-
val c = new C
152-
println(s"E: ${c.x1}, ${c.x2}, and $x3...")
164+
class LogAdder extends Adder {
165+
private object state {
166+
var added: Set[Int] = Set.empty
167+
}
168+
import state._
169+
override def add(x: Int): Unit = { added += x; super.add(x) }
153170
}
154-
//scala> new E
155-
//D: null but goodbye
156-
//A: null, null
157-
//B: hello, null
158-
//C: hello, dad
159-
//E: hello, dad, and goodbye...
160171

161-
Sometimes all you need from an interface is a compile-time constant.
172+
### Early definitions: deprecated
173+
174+
Scala 2 supports early definitions, but they are deprecated in Scala 2.13 and unsupported in Scala 3.
175+
See the [migration guide](https://docs.scala-lang.org/scala3/guides/migration/incompat-dropped-features.html#early-initializer) for more information.
176+
177+
Constant value definitions (specified in SLS 4.1 and available in Scala 2)
178+
and inlined definitions (in Scala 3) can work around initialization order issues
179+
because they can supply constant values without evaluating an instance that is not yet initialized.
162180

163-
Constant values are stricter than strict and earlier than early definitions and have even more limitations,
164-
as they must be constants. They are specified in SLS 4.1.

0 commit comments

Comments
 (0)