问题
The following code
abstract class Table(val name: String) {
val columns: List[Column]
def getAliasColumns: String = {
val reallyThere = columns.forall(c => c != null)
println("Columns really there: " + reallyThere)
if (reallyThere == false) {
println(" columns " + columns)
}
columns.map(c => s"${c.table.name}.${c.name} as ${c.table.name}_${c.name}")
.mkString(", ")
}
}
class Column(val table: Table, val name: String, val foreignKey: Option[Column])
object Column {
def apply(table: Table, name: String): Column = {
new Column(table, name, foreignKey = None)
}
def apply(table: Table, name: String, fk: Column): Column = {
new Column(table, name, Some(fk))
}
}
object Domain {
object Tenant extends Table("Tenant") {
object Columns {
// Primary key
val Id = Column(Tenant, "id")
// Just a name
val Name = Column(Tenant, "name")
}
val columns = List(Columns.Id, Columns.Name)
}
object Node extends Table("Node") {
object Columns {
// Primary key
val Id = Column(Node, "id")
// Foreign key to another table
val TenantId = Column(Node, "tenantId", Tenant.Columns.Id)
// Foreign key to itself
val NodeId = Column(Node, "nodeId", Id)
// Just a name
val Name = Column(Node, "name")
}
val columns = List(Columns.Id, Columns.TenantId,
Columns.NodeId, Columns.Name)
}
val tables = List(Tenant, Node)
}
works, if the order to access the information is:
object RecursiveObjects extends App {
Domain.tables.foreach(t => println(t.getAliasColumns))
println(Domain.Node.getAliasColumns)
}
and the output is as expected:
Columns really there: true
Tenant.id as Tenant_id, Tenant.name as Tenant_name
Columns really there: true
Node.id as Node_id, Node.tenantId as Node_tenantId, Node.nodeId as Node_nodeId, Node.name as Node_name
but it fails, if the order is reversed:
object RecursiveObjects extends App {
println(Domain.Node.getAliasColumns)
Domain.tables.foreach(t => println(t.getAliasColumns))
}
and in this case the output is
Columns really there: true
Node.id as Node_id, Node.tenantId as Node_tenantId, Node.nodeId as Node_nodeId, Node.name as Node_name
Columns really there: false
columns List(null, null)Exception in thread "main" java.lang.NullPointerException
using Scala 2.10.1
Some background information:
- The object definitions describe the logical data model for a RDBMS.
- Tables know their columns (children), each column knows his table (parent)
- A foreign key column has an optional property describing the primary key column in the parent table
- This relationship can be recursive (the nodes table is recursive)
- The individual constants for tables and columns are required.
- If possible, I'd like to avoid var
I've found a section in the language specification (5.4)
Note that the value defined by an object definition is instantiated lazily.
which is actually required to be able to setup it at all. I assume "value" means the object as a whole, in contrast to its "values" (properties).
Anyway the instance for the columns property is obviously created, but its elements are not yet "materialized", this is visible in the output of the 2nd run.
I have tried to solve it using early definitions, but in this case the compiler complains with illegal cyclic reference involving object, so this does not compile:
object Node extends {
val columns = List(Domain.Node.Columns.Id, Domain.Node.Columns.TenantId,
Domain.Node.Columns.NodeId, Domain.Node.Columns.Name)
} with Table("Node") {
object Columns {
// Primary key
val Id = Column(Node, "id")
[...]
}
}
So my questions are:
- Why does it fail?
- In which state is the columns property (the list exists, but the elements are null)?
- How can I setup this properly? Or should I just materialize it in a defined order as a workaround due to its cyclic/recursive nature?
Update/solution
Based on 0__'s answer: The solution consists of declaring the columns property as lazy and changing the abstract val into a def.
As for the state of the columns property: If you put -Xcheckinit into scalac's command line options, additional runtime checks are added. And in this case the following error turns up:
Caused by: scala.UninitializedFieldError: Uninitialized field: RecursiveObjects.scala: 35
This error is otherwise silently ignored, and so the list just contains nulls.
回答1:
val
initialization in Scala is terrible, I come across these NPEs all the time. It is probably all fine by the intricacies of the spec, but from a practical view point, they are really broken.
My advise is to never use public vals unless they are initialised without reference to other fields or objects, but make them all lazy. In this case, if you use
lazy val columns = ...
in Tenant.Columns
and Node.Columns
, it should work as expected.
I'm not sure what the exact initialisation in your case is, but from the nulls, I think calling Domain.Node
implies here that Domain
and/or Domain.Tenant
is not yet properly initialised. For example, if you add a seemingly dummy statement Domain;
in front of your second example, it will also succeed (because that makes Domain
initialise first).
Here is a related question/answer with a link showing how to identify problems with strict value initialisation.
来源:https://stackoverflow.com/questions/16773809/initialization-order-of-values-in-objects-how-to-setup-cyclic-recursive-objects