Has it always been possible to refer to `self` in init before and part of assignment of the last stored property?

This surprised me… a lot. So much that I’ve come to question my identify as a seasoned Swift developer… so I’m curious, has this ALWAYS worked, just that I’ve missed it? Or has a change happened along the way (since 2014)?

final class MyViewModel {
	var boolProxy: Bool
	init(boolProxy: Bool) {
		self.boolProxy = boolProxy
	}
}

final class MyView: NSObject {
    var bool = true
}

final class VC: NSObject {
	let view: MyView
	let vm: MyViewModel
	override init() {
		self.view = MyView()
		// Today I learned that I can use `self.` on row below, even though `self.vm` is not
		// set "to the right of `=`".
		self.vm = MyViewModel(boolProxy: self.view.bool)
		super.init()
	}
}

Or more generally:



struct Snapshot {
	let boolSnapshot: Bool
	init(boolSnapshot: Bool) {
		self.boolSnapshot = boolSnapshot
	}
}

struct SourceOfTruth {
	var bool = true
}

struct Holder {
	let snapshot: Snapshot
	let truth: SourceOfTruth
	init() {
		self.truth = SourceOfTruth()
		// Today I learned that I can use `self.` on row below, even though `self.vm` is not
		// set "to the right of `=`".
		self.snapshot = Snapshot(boolSnapshot: self.truth.bool)
	}
}

I would have thought we could not reference self part of setting the last stored property, since until set “self” cannot be referenced (was my incorrect belief).

But you are not using self.. you are using self.truth etc - that was already initialised.
If you were to use self (e.g. print(self)) that would be an error.
I think it was always like this.

4 Likes

I asked the AI overlords when we were able to stop using redundant local variables for what you demonstrated, and it could not give me a definitive answer.

Stored properties work that way. Instance computed properties, like methods, do not. It seems to me that they should.

struct Instance {
  var computed: Void { }
  let stored: [Void]
  init() {
    computed // ❌ 'self' used before all stored properties are initialized
    stored = []
    computed
  }
}

IIUC it has always worked this way, at least back to Swift 2.x.

The definite initialization pass tracks "uses" of memory locations in a granular way. The initialization state of every "element" of an aggregate is tracked independently, and the pass enforces that uses are only allowed if the relevant set of elements in the use are all known to already be initialized (on all control flow paths that reach that point). This explains the behavior in your examples since, e.g., self.truth is set before it's read to initialize self.snapshot. If it were only conditionally initialized though, it would not be allowed:

struct Holder {
  let snapshot: Snapshot
  let truth: SourceOfTruth
  init() {
    if Bool.random() {
      self.truth = SourceOfTruth()
    }
    self.snapshot = Snapshot(boolSnapshot: self.truth.bool) // 🛑 error: 'self' used before all stored properties are initialized
  }
}

Sort of related – you might find this somewhat recent discussion on how this logic applies to tuples of interest.

I'm not sure computed properties or methods could or should work this way because they both could perform arbitrary accesses of the instance's stored properties, and in general their implementations could be opaque to the compiler, so it must conservatively require full initialization before they can be called.

5 Likes

I wanted to emphasize that link that @jamieQ posted is to swiftfiddle.com, which is a very useful online playground for Swift. It allows you to try versions of Swift going back to Swift 2.2, the first open source version.

This is a great resource for answering questions like “Has this always worked this way?” Or “When did this start working this way?”

7 Likes

Agree. We probably should bring these, at least the major ones, to Godbolt as well. @jamieQ, I think you were the one adding the most recent versions to Godbolt. Do you see any reason why we can’t do this with older versions that haven’t been added yet?

1 Like

I don't see why not. The primary barrier for me personally doing this the last time I thought about it was divining what the right URLs are to get the Ubuntu artifacts. Luckily, the LLMs have now helped figure that out. I'm not sure since they're associated with such an old version of Ubuntu if there will be issues running them on the compiler explorer hosts, but I guess we'll find out. Opened some draft PRs: 1 & 2.

2 Likes

Right, I've always thought about self.property as using self, in the particular case I’ve always went through the init-dance of a let instanceOfClass and later self.instanceOfClass = instanceOfClass - entirely meaningless!

Thanks!

Let’s see what happens, thanks! Also, do you know why the "Execution only" option goes only back to Swift 5.0?

Digression...

No, not really sure other than the superficial explanation that it's explicitly disabled in the config. Execution support seems to have been added here and one of the commits says that those disabled older releases had runtime errors.

1 Like