The short answer on why you get a NullPointerException
is, that initialization of C
requires initializing b
, which invokes the method stored in val foo
, which is not initialized at this point.
Question is, why is foo
not initialized at this point? Unfortunately, I cannot fully answer this question, but I'd like to show you some experiments:
If you change the signature of C
to extends B
, then B
, as the superclass of C
is instantiated before, leading to no exception being thrown.
In fact
trait A {
val initA = {
println("initializing A")
}
}
trait B extends A {
val initB = {
println("initializing B")
}
}
class C(a: String) {
self: B => // I imagine this as C has-a B
val initC = {
println("initializing C")
}
}
object Main {
def main(args: Array[String]): Unit ={
val x = new C("34") with B
}
}
prints
initializing C
initializing A
initializing B
while
trait A {
val initA = {
println("initializing A")
}
}
trait B extends A {
val initB = {
println("initializing B")
}
}
class C(a: String) extends B { // C is-a B: The constructor of B is invoked before
val initC = {
println("initializing C")
}
}
object Main {
def main(args: Array[String]): Unit ={
val x = new C("34") with B
}
}
prints
initializing A
initializing B
initializing C
As you can see, the initialization order is different. I imagine the dependency-injection self: B =>
to be something like a dynamic import (i.e., putting the fields of an instance of B
into the scope of C
) with a composition of B
(i.e., C
has-a B
). I cannot prove that it is solved like this, but when stepping through with IntelliJ's debugger, the fields of B
are not listed under this
while still being in the scope.
This should answer the question on why you get a NPE, but leaves the question open on why the mixin is not instantiated first. I cannot think of problems that may occur otherwise (since extending the trait does this basically), so this may very well be either a design choice, or noone thought about this use case. Fortunately, this will only yield problems during instantiation, so the best "solution" is probably to not use mixed-in values during instantiation (i.e., constructors and val
/var
members).
Edit: Using lazy val
is also fine, so you can also define lazy val initC = {initB}
, because lazy val
are not executed until they are needed. However, if you do not care about side effects or performance, I would prefer def
to lazy val
, because there is less "magic" behind it.