I found this inconsistency in property initialization of structs, depending whether the synthesized memberwise initializer or custom initializer is used:
func generateDefaultName() -> String {
print("Generating default name")
return "Anonymous"
}
struct User1 {
var name = generateDefaultName()
}
struct User2 {
var name = generateDefaultName()
init(name: String) {
self.name = name
}
}
let user1 = User1(name: "User1") // User1.name's default value is not assigned, `generateDefaultName()` is not called. `user1.name` is directly set to "User1".
let user2 = User2(name: "User2") // User2.name's default value is assigned with the result of calling `generateDefaultName()`. `user1.name` is set to the default value first and then "User2". This is the expected behavior to my understanding.
To be clear, I personally think synthesized memberwise initializer skipping default property value assignment is a bug, not that custom initializer doing default property value assignment is a bug.
People who are relying on their initial value setter not being called due to using a synthesized initializer rather than an explicit one were asking for trouble
Thanks for digging down. Consider these use cases:
struct S {
var other: String
var name: String = sideEffect()
// Obviously sideEffect is called once:
init(other: Sting, name: String) {
_ = self.name // Obviously allowable here
self.other = other
}
// Obviously sideEffect is called twice:
init(other: String, name: String = sideEffect()) {
_ = self.name // Obviously allowable here
self.other = other
}
// here is where we are not sure:
init(other: Sting, name: String) {
_ = self.name // should this be allowable here?
self.other = other
self.name = name
}
// here is where we are not sure:
init(other: String, name: String = sideEffect()) {
_ = self.name // should this be allowable here?
self.other = other
self.name = name
}
}
In init 1 & init 2 it's obvious what should happen.
I believe for simplicity of reasoning init 3 and init 4 should behave the same, otherwise the logic is too convoluted. In other words this:
if there's explicit initialiser that matches the signature of what
autosynthesized initialiser would have been, that does assign
the property in question to the particular named parameter and
doesn't use that property beforehand – then follow the
autosynthesized initialiser rule, otherwise do what we currently do.
is much more complicated than this:
member wise initialiser rule has to change to call the side effect.
That is if we do want to align the two.
The crucial here if we should allow "_ = self.name" before name is being set in the initialiser – I think we should – but then the rule whether sideEffect is called or not becomes complicated (e.g. the mere presence of "_ = self.name" makes a spooky behaviour change).
struct User1 {
var name: String
init(name: String = generateDefaultName()) {
self.name = name
}
}
This is obviously a correct formulation, and it never needs calculate the default if the caller supplies a name itself. This can be important if generateDefaultValue() is expensive.
The fact that this:
struct User2 {
var name = generateDefaultName()
init(name: String) {
self.name = name
}
}
Isn't sugar for this:
struct User2 {
var name: String
init(name: String = generateDefaultName()) {
self.name = name
}
}
Or for this:
struct User2 {
var name: String
init(name: String) {
self.name = name
}
init() {
name = = generateDefaultName()
}
}
Is an obvious deficiency in the code that synthesizes the default initializer.
The best way to solve this compatibly for people who were relying on the erroneous behavior, is to take advantage of the fact that we already do liveness tracking for individual fields in an initializer.
The compiler should, instead of erroring if you access a member in an init before that member has been initialized, first check if it has a default value. If it does, assign that default value before the access, otherwise emit the usual error.
Upon reflection I take this back. The algorithm could be simplified to this:
if self used within the initialiser (e.g. print(self)) - check if there are any stored properties not yet initialised - for those see if they have initial values - initialise those properties with their initial values - check if there are still uninitialised fields - if so bail with error, otherwise proceed.
if self.property is used within initialiser (e.g. print(self.property)) perform the above step with just that one property.
at the end of initialiser pretend self being used somehow and perform step 1.
With this approach the initial values will be executed as late as possible and only if needed. It would be a breaking change with the current behaviour, but it seems like it will do more good than bad going forward. +1 to consider the current behaviour a bug worth fixing.