Writing to a static property causes its default value to be evaluated - is this deliberate? A bug? Could it be changed?

enum Foo {
    static var bar: Int = { print("bar default value evaluated"); return 0 }()
    static var baz: Bool = bar % 2 == 0
}
func demo () {
    print("a")
    Foo.bar = 7
    print("b")
    print(baz)
}
/*
prints:
a
bar default value evaluated
b
false
*/

I would have assumed that the default value would not be evaluated if the first thing I do with the static property is to set a new value, and that therefore the console would read:

a
b
false

Can anyone tell me more about this? I think I would prefer that it behaved in the way I was expecting, although I suppose it doesn't matter all that much.

1 Like

I imagine this is because bar could have a didSet implemented, which receives the oldValue which would need to be initialized to something. So I don't think it's a bug, but perhaps something that could be optimized away in the case where a didSet does not exist.

In general I think Swift errs on the side of making sure things are initialized, especially in the case of a static var where the initialization will occur exactly once so the wasted effort / performance hit won't add up over time.

Edit:
I think the current behavior also avoids a subtle side effect that would appear if such an optimization was introduced. It might be surprising to some that by adding a didSet to foo would cause the default initialization code to run when it previously did not. Sort of a spooky action at a distance that could subtly alter the semantics of your program, especially if the initializer has side effects.

2 Likes

See [SR-1178] Closure for initializing static variables is evaluated even when the variable is set with a different value · Issue #43786 · apple/swift · GitHub and @hamishknight’s answer here: In Swift, why does assigning to a static variable also invoke its getter - Stack Overflow

1 Like

Note that we no longer fetch the oldValue if the body of the didSet does not have a reference to it or if it’s not explicitly requested in the parameter list aka didSet(oldValue) (SE-0268).

1 Like

I think this is very interesting with regard to this possible concern:

because now I think that there is a very coherent, non-spooky new behavior that we could change to:

If the first time that you access a static variable is to set its value then in most cases the default value provided at the property definition will not be evaluated (which can be seen as logical and in-line with, for example, the fact that a default function argument is not evaluated if the caller supplies a value). The only condition under which setting the static value causes the default value to be evaluated is not merely the presence of a didSet, but more specifically the presence of a didSet that explicitly requests access to the oldValue.

To me it seems like explicitly requesting a guaranteed (non-optional) oldValue in the didSet is equivalent to requesting that the initial value be brought into play, which makes the fact that it now is evaluated whereas it previously wasn't more obvious than spooky.

2 Likes

I kind of want to go in a completely opposite direction with this. Should static properties really be implicitly lazy?

  1. With constant expressions, there’s no such thing as lazy initialization. Should Swift have two different flavors of static property differentiated by the capabilities of dataflow analysis?
  2. Initializer expressions can have side effects. It’s not obvious—especially for programmers familiar with other languages that have a static keyword—that every static property implies two states: one in which the side effects have been run, and one in which they haven’t. The proposal of skipping the initializer expression entirely when assigning threatens to persist that first hidden state.
2 Likes

Static properties are lazy to avoid C++'s static initialization order fiasco. Proper compile time constants wouldn't suffer that problem anyway, which is why C++20 added constinit. I think it's perfectly reasonable for compile time (or compile time initialized) Swift static variables to be initialized differently than regular ones. Not that we're likely to get them any time soon.

2 Likes

Global/static variables with constant expression initializers can and do get optimized into constant initializers already. It would be reasonable to have an annotation that requires the initializer to be constant-foldable. For side-effectful initializers, initialization on first use is less bad than any of the alternatives (as @Nobody1707 noted). C++11 also uses initialization on first use for local static variables.

6 Likes

I would argue that unconditional initialization on first access, whether read or write, is similarly less bad than the alternative of conditional initialization based on whether the first access is a read or a write. Otherwise the program has two possible branches of state that can change based on adding, removing, or reordering a read from an otherwise-idempotent getter.

8 Likes