Reputation: 171
If a class inherits from a trait, and they both share a parent, it seems as if overriding an attribute of the parent will make it inaccessible from the trait. Namely
class TestClass(arg: String) extends Parent(arg) with TestTrait {
override val foo = "hey"
}
trait TestTrait extends Parent {
val bar = foo
}
abstract class Parent(arg: String) {
val foo = arg
}
Then running
val c = new TestClass("hello")
c.bar
will return null
. This seems like counterintuitive behavior for me. Could anyone explain what the general inheritance rules and rationale behind them are to me?
EDIT: Thanks for the responses. However, I'm still confused as to why this works then:
class TestClass(arg: String) extends Parent(arg) with TestTrait {
// override val foo = "hey"
}
trait TestTrait extends Parent {
val bar = foo
}
abstract class Parent(arg: String) {
val foo = arg
}
Running the same as before will successfully produce "hello". Based on the explanations provided, I would've have expected null
again. Sorry for not provided this context in the original phrasing.
Upvotes: 1
Views: 909
Reputation: 9705
Normally, val
s are evaluated in sequence, meaning both in-type declaration sequence, and inheritance declaration sequence.
If there would be no override
, bar
would be evaluated based on the definition from the Parent
constructor, since it is executed before the TestTrait
constructor (because TestTrait
is a subtype of Parent
). So, bar
would have whatever value it has in Parent
.
However, since you override foo
in TestClass
, the evaluation takes place only once TestClass
's constructor gets invoked, and this is after TestTrait
.
The best rationale I could find for this in the SLS is within 5.3.1:
The signature and the self constructor invocation of a constructor definition are type-checked and evaluated in the scope which is in effect at the point of the enclosing class definition, augmented by any type parameters of the enclosing class and by any early definitions of the enclosing template. The rest of the constructor expression is type-checked and evaluated as a function body in the current class.
(emph. mine)
Implying the following pseudocode:
class TestClass(arg: String) extends Parent(arg) with TestTrait {
Parent(arg) //arg is ignored, since TestClass overrides foo.
TestTrait() //foo is still null at this point
override val foo = "hey"
}
Essentially boiling down to the same case as:
class A {
val a: String = b //a will be null, since b is not yet evaled
val b = "A"
}
(only without the luxury of a compiler warning)
In hindsight, this quirk is in fact hinted by Scala's language feature of early definitions. With your code, its use would look like the following:
class TestClass(arg: String) extends {
override val foo = "hey"
} with Parent(arg) with TestTrait
trait TestTrait extends Parent {
val bar = foo
}
abstract class Parent(arg: String) {
val foo = arg
}
scala> new TestClass("hello").bar
res0: String = hey
Upvotes: 1
Reputation: 68660
That's because constructors are executed top to bottom (from less derived type to most derived type).
Which means TestTrait
is constructed before TestClass
-> which means foo
is null while TestTrait
is being constructed.
Parent (arg = null, foo = null)
\
\
TestTrait (bar = null) Parent(arg = "hello", foo = "hello")
\ /
\ /
TestClass(arg = "hello", foo = "hey")
This may seem surprising, because dynamic dispatch works the other way around - methods are called on the most derived type first, and then on less derived types (if super.method()
is invoked).
But there's a good reason for this: the initialization of a type often depends on the correct initialization of its base type.
Upvotes: 1