Why can't a property initialiser use 'this', not even 'this.width'?

← Classes and OOP · Ref: Q2180

A property initialiser may not use 'this' at all - not as a value, not in a capture list, and not to reach a property ('this.width'). Write the property's plain name instead: 'height as Integer: width * 2'.

WHY: TWO-PHASE CONSTRUCTION

Building an object has two phases. Phase one gives every property a value: the super constructor runs, then this type's property initialisers in declaration order, then the constructor body. Phase two begins only when EVERY property has a value - the object is complete - and only then may anything use it: call its methods, hand it out, let something else hold it. A property initialiser always runs in phase one, before the constructor body, so the object is NEVER complete while an initialiser runs. 'this' there is a half-built object: a method called through it, or a dynamic function holding it, could reach a property that has not been given its value yet (in Java that compiles and fails with a NullPointerException, or sees a 0 that is not the real value).

WHAT AN INITIALISER MAY READ

Only a property declared EARLIER that has its own initialiser - that initialiser has already run (anything else is E08060). Read it by its plain name: 'height as Integer: width * 2'. A capture list in an initialiser is evaluated by the initialiser, so it follows the same rule: '(factor: width)' captures the earlier property's object, while '(owner: this)' would capture the half-built object (E08061). A value that needs more comes from a function ('depth as Integer: defaultDepth()') or from a constructor argument ('label: name' in the constructor).

WHY NOT ALLOW 'this.width', WHICH READS THE SAME FIELD?
Because one rule with no exceptions is easier to see and to keep - for people and for AI - than a list of allowed uses. Once 'this' may appear in an initialiser, the next edit is 'this.describe()', '$this' or '(owner: this)', each of which uses the half-built object. So the rule is simply: no 'this' in an initialiser, and E08061 tells you to write 'width'.

When the object needs a reference to itself - a child that points back to its parent - build it in the CONSTRUCTOR, where EK9 allows it in one exact shape: see Q2181. For calling the object's own methods during construction see Q1396; for the order initialisers run in see Q1397.

Example

defines module qa.oop.no.this.in.initialiser

  defines function

    Scaler() as abstract
      -> amount as Integer
      <- rtn as Integer?

    defaultDepth()
      <- rtn as Integer: 4

  defines class

    Box
      width as Integer: 3
      height as Integer: width * 2
      depth as Integer: defaultDepth()
      scale as Scaler: (factor: width) is Scaler as function (rtn: amount * factor)
      label as String?

      default private Box()

      Box()
        -> name as String
        label: name

      width() as pure
        <- rtn as Integer: width

      volume()
        <- rtn as Integer: width * height * depth

      scaled()
        -> amount as Integer
        <- rtn as Integer: scale(amount)

      describe()
        <- rtn as String: `${label}: ${width} x ${height} x ${depth}`

      default operator ?

  defines program

    NoThisInInitialiser()
      stdout <- Stdout()
      box <- Box("crate")
      stdout.println(box.describe())
      stdout.println(`volume: ${box.volume()}`)
      stdout.println(`scaled 10: ${box.scaled(10)}`)
      stdout.println(`width: ${box.width()}`)

Common mistakes

E08061 — A property initialiser runs before the constructor, while the object is still being built, so it may not use 'this' at all - even to reach a property. Write the property's plain name: 'width * 2' reads the earlier, already-initialised 'width'.

Incorrect:

height as Integer: this.width * 2

Correct:

height as Integer: width * 2

E08061 — The capture list is evaluated by the initialiser, so '(owner: this)' would hand the half-built object to the function. Capture the earlier property itself, '(factor: width)', or build the function in the constructor once the object is complete.

Incorrect:

(owner: this) is Scaler as function (rtn: amount * owner.width())

Correct:

(factor: width) is Scaler as function (rtn: amount * factor)
Other ways to ask this
  • Why is 'this.width' in a property initialiser E08061?
  • Can I use this in a property initialiser in EK9?
  • Can a property initialiser capture 'this' in a dynamic function?
  • What is two-phase construction in EK9?
  • How does a property initialiser read another property of the same object?

Coming from another language?

Java: a field initialiser may use 'this' freely - pass it out, call overridable methods - and the bugs show up at run time as nulls or zeros. Kotlin: property initialisers may reference 'this'; leaking it is only a warning. Swift: a stored property's default value cannot reference 'self' at all (a compile error) - EK9 takes the same position and adds that the constructor's use of 'this' waits until every property is set. C#: a field initializer cannot use 'this' or read another instance field at all; EK9 is a little more lenient - an initialiser may read an EARLIER initialised property by its plain name - but never through 'this'.

Keywords: property, half-built, field, initializer, capture, initialiser, construction, constructor, E08060, E08061, two-phase