didSet is not triggered while called after super.init()

Hello everyone. I have met the following problem. Here are two logically identical (as for me) pieces of code. The first example sets value after calling super.init to trigger its didSet.

class Base {}

class Child: Base {
  var value: Int? {
    didSet {
      print("it changed")
    }
  }

  init(initialValue: Int?) {
    super.init()
   
    value = initialValue // setting after initialization
  }
}

The second one does the same, but the assignment is wrapped in a method.

class Base {}

class Child: Base {
  var value: Int? {
    didSet {
      print("it changed")
    }
  }

  init(initialValue: Int?) {
    super.init()
   
    updateInitialValue(initialValue)  
  }

  func updateInitialValue(_ initialValue: Int?) {
    value = initialValue // setting after initialization
  }
}

I thought the both pieces should work identically, but actually the first option never triggers didSet, what seems a bug to me. Is it the expected behavior? If so, does not this makes a possibility to a misuse?

Apple documentation says: "The willSet and didSet observers of superclass properties are called when a property is set in a subclass initializer, after the superclass initializer has been called. They aren’t called while a class is setting its own properties, before the superclass initializer has been called."

https://docs.swift.org/swift-book/LanguageGuide/Properties.html

That suggests that it should be called here, since the superclass initializer has been called.

It looks like the compiler traits setting properties inside init body as if we always initialize them, ignoring their position, before or after super.init() they are set.

So: is this a compiler bug or a documentation bug - does anyone know?

Neither: both the compiler's behavior and the text are correct as written [edit: in that they don't contradict each other; @Slava_Pestov rightly points out it's not necessarily what we want].

The quoted text has nothing to do with the OP's example, because the quoted text is talking about observers of superclass properties, and the OP is not setting a property of the superclass.

I agree that this behavior is weird. My understanding is that observers are not called when initializing stored properties, but should be called once object is fully initialized.

This is a long-standing known bug in the compiler. The decision whether to call the setter or store to the address needs to be flow-sensitive.

A while ago @hborla fixed the analogous issue with property wrappers by adding an assign_by_wrapper SIL instruction which lowers to either a setter call or a store to an address in the definite initialization pass. So the general idea is already implemented, it just needs to be slightly generalized to be used for observers too (other kinds of properties like computed properties cannot be assigned at all prior to the super.init() call).

However since there is source compatibility impact it should probably wait until we do a new -swift-version mode.

4 Likes

Wow, thank you so much for such a detailed explanation!

The quoted text also says: "They aren’t called while a class is setting its own properties, before the superclass initializer has been called." - which suggests that they are supposed to be called when a class sets it own properties after the superclass initialiser has been called (and the class is already fully initialised). Anyway, @Slava_Pestov has confirmed this is a compiler bug.