hj-core
hj-core

Reputation: 118

initialization of top level properties

I encounter some questions about kotlin top-level properties and below is the sample code:

// Situation 1

class TreeNode(var `val`: Int) {
    var left: TreeNode? = null
    var right: TreeNode? = null
}

val tree2 = TreeNode(2)
val tree1 = TreeNode(3).apply { left = tree2 } 

fun main() {
    println(tree1.left) // prints "TreeNode@..."
}
// Situation 2

class TreeNode(var `val`: Int) {
    var left: TreeNode? = null
    var right: TreeNode? = null
}

val tree1 = TreeNode(3).apply { left = tree2 } 
val tree2 = TreeNode(2)

fun main() {
    println(tree1.left) // prints "null"
}
// Situation 3

class TreeNode(var `val`: Int) {
    var left: TreeNode? = throw Exception("I am an exception")
    var right: TreeNode? = null
}

val tree1 = TreeNode(3).apply { left = tree2 } 
val tree2 = TreeNode(2)

fun main() {
    /* prints "Exception in thread "main" java.lang.ExceptionInInitializerError 
     * Caused by: java.lang.Exception: I am an exception ..."
     */
    println(tree1.left) 
}

It seems that

  1. top-level properties are initialized from top to bottom;
  2. if the depended property is not initialized, it will be replaced by the default value;

Am I right? And if it is, I wonder

  1. is there official document saying this behavior?
  2. why it is prefered to replaced the depended property with default value if it is not yet initialized, instead of having a compile time error or throwing a runtime error says that tree2 is not initialied?

Thanks!

Upvotes: 4

Views: 1011

Answers (1)

Sweeper
Sweeper

Reputation: 271310

top-level properties are initialized from top to bottom;

Actually, I can't find anything confirming this on the Kotlin/Core spec. I've only been able to find this Kotlin forum post that says top-level property initialisation is platform-dependent, and that the only guarantee you get is the property's initialiser will be run before you access the property, which doesn't say anything about any other property's initialisers.

Kotlin doesn’t give you any guarantees when top level properties will be initialized. All you can say is that the initialization code will be called before you access a property, but you can’t necessarially tell when.

If you know which target platform you are on, you have a bit more information. I don’t know enough about native to help you there but on the JVM a top level property get’s initialized the when you call any top level function or property within the file. It’s a bit more complicated than that. It has to do with the JVM classloader and when that calls static initialization blocks ( function).

Not gonna lie, this is surprising for me too! But from my testing, it seems like the current compiler version, 1.7.10, does initialise top-level properties on all platforms (JVM, JS, Native), unless they are inlined.


if the depended property is not initialized, it will be replaced by the default value

This is not true either. If a depended property is not initialised, the value it gets is unspecified. According to the spec (emphasis mine):

The main difference between declaration scopes and statement scopes is that names in the statement scope are bound in the order of appearance. It is not allowed to access a value through an identifier in code which (syntactically) precedes the binding itself. On the contrary, in declaration scopes it is fully allowed, although initialization cycles may occur leading to unspecified behaviour.

The top-level scopes of non-script Kotlin files are "declaration scopes", as specified a bit earlier on that page.


Since you seem to be on JVM, what actually happens (but not specified) in Situation 2 is that the uninitialised tree2 is null (default value for all reference types on the JVM), and assigned to left, which also happens to be null at that time.

For Situation 3, there are no platform-specific behaviour at all. You call the constructor for Tree, then the initialiser for left runs, and the exception is thrown, and that's the end of the program. It doesn't even get to the apply part.

A sufficient demonstration of the unspecified behaviour of uninitialised properties is

fun main() {
    println(y)
}

val y = run { x }
val x = run { 1 }

On the JVM, it prints 0. On Kotlin/Native it also prints 0, though I'd imagine it could also print whatever bits happens to be in that location. On Kotlin/JS, it prints "undefined".


As for why it is designed to be "unspecified", it's probably because the Kotlin team has other, more important things to focus on. People usually don't have too many interdependent global properties lying around anyway. See also this great answer by Eric Lippert.

Upvotes: 3

Related Questions