In many object-oriented languages, it is common to accept parameters in a class constructor for the purpose of assigning them to class members:
class MyClass(param1, param2, ...) { val member1 = param1 val member2 = param2 ... }
Scala, which favors concise code, lets you avoid this redundancy by declaring members and constructor parameters in one go:
class MyClass(val member1, val member2, ...) { ... }
What is the result of executing the following code?
trait A { val audience: String println("Hello " + audience) }
class BMember(a: String = "World") extends A { val audience = a println("I repeat: Hello " + audience) }
class BConstructor(val audience: String = "World") extends A { println("I repeat: Hello " + audience) }
new BMember("Readers") new BConstructor("Readers")
Hello Readers I repeat: Hello Readers Hello Readers I repeat: Hello Readers
Hello World I repeat: Hello Readers Hello World I repeat: Hello Readers
Hello null I repeat: Hello Readers Hello Readers I repeat: Hello Readers
Hello null I repeat: Hello Readers Hello null I repeat: Hello Readers
The key question here is when precisely the assignment of "Readers" to audience becomes visible. You may also wonder whether or how the default value, "World", is involved. Surely the small optimization of moving the member declaration of audience into the constructor parameter list has no impact, though? Not so—the correct answer is number 3:
scala> new BMember("Readers") Hello null I repeat: Hello Readers res3: BMember = BMember@1aa6f6eb
scala> new BConstructor("Readers") Hello Readers I repeat: Hello Readers res4: BConstructor = BConstructor@64b6603a
In other words, the value of audience in A differs if the member is declared in B's constructor parameters, as opposed to the constructor body.
To understand the difference between member declarations in the class body versus in the constructor parameter list, you need to examine Scala's class initialization sequence. Consider again the class declarations:
class BMember(a: String = "World") extends A { ... }
class BConstructor(val audience: String = "World") extends A { ... }
Both class declarations are of the form:[1]
class c(param1) extends superclass { statements }
According to the language specification,[2] the initialization sequence for new BMember("Readers") and new BConstructor("Readers") will be:
Here, note that we are omitting details regarding traits, etc., that do not apply to this example. In the case of BMember, "Readers" is assigned to the constructor parameter, a, in the first step. When A's constructor is invoked, audience is still uninitialized, so the default string value null is printed. "Readers" is assigned to audience, and then printed, only when the statement sequence in the body of BMember executes.
BConstructor's case is different: here "Readers" is evaluated and assigned to audience straight away, as part of the evaluation of the constructor arguments. The value of audience is already "Readers" by the time A's constructor is invoked.
In general, the pattern in BConstructor is preferred as its behavior leaves less room for surprises. The val declared in the superclass never exists in an uninitialized state.
You can achieve the same result without declaring audience in the constructor parameter list by using an early field definition[5] clause. This allows you to perform additional computations on the constructor arguments (e.g., normalizing the case of string arguments), or to create anonymous classes with correctly initialized values:
class BEarlyDef(a: String = "World") extends { val audience = a } with A { println("I repeat: Hello " + audience) }
scala> new BEarlyDef("Readers") Hello Readers I repeat: Hello Readers res7: BEarlyDef = BEarlyDef@44c93da7
scala> new { val audience = "Readers" } with A { println("I repeat: Hello " + audience) } Hello Readers I repeat: Hello Readers res0: A = anon1@71e16512
Early definitions define and assign member values before the supertype constructor is called. The initialization sequence, as per the sections of the language specification applicable in this case,[6] is:
A class is initialized by evaluating the template:
In short, superclass and supertrait initialization code is executed after parameter evaluation and early field definitions and before the initialization statements of the class or trait being instantiated. The direct superclass and mixed-in traits are initialized in left-to-right order as they appear in the class, trait, or object definition.
An extended code sample puts all of this together:
trait A { val audience: String println("Hello " + audience) }
trait AfterA { val introduction: String println(introduction) }
class BEvery(val audience: String) extends { val introduction = { println("Evaluating early def"); "Are you there?" } } with A with AfterA { println("I repeat: Hello " + audience) }
scala> new BEvery({ println("Evaluating param"); "Readers" }) Evaluating param Evaluating early def Hello Readers Are you there? I repeat: Hello Readers res3: BEvery = BEvery@6bcc2569
Think of superclass constructors and supertrait initializers as being inserted, in left-to-right order of declaration, after the opening bracket of the class or object body (which forms the primary constructor). |
[1] Odersky, The Scala Language Specification, Section 5.3. [Ode14]
[2] Odersky, The Scala Language Specification, Section 5.1. [Ode14]
[3] Odersky, The Scala Language Specification, Section 5.1. [Ode14]
[4] A template is the body of a class, trait, or singleton object definition. It defines the type signature, behavior, and initial state of a class, trait, or object.
[5] Odersky, The Scala Language Specification, Section 5.1.6. [Ode14]
[6] Odersky, The Scala Language Specification, Sections 5.1.1, 5.1, and 5.1.6. [Ode14]