Initialization order of values in objects: How to setup cyclic/recursive objects properly?

我们两清 提交于 2019-12-11 02:22:51

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!