The semantics of property observer when used with property wrapper

It looks like the compiler attaches the willSet observer to the computed property n, not the stored property _n. So your code:

struct Bar {
    @Foo var n = 1 {
        willSet {
            print(newValue)
        }
    }
}

gets translated into this pseudocode (pseudcode because computed properties can't have observers, so the willSet code would be included in the setter):

struct Bar {
    private var _n: Foo = Foo(wrappedValue: 1)
    var n: Int {
        get { _n.wrappedValue }
        willSet {
            print(newValue)
        }
        set {
            _n.wrappedValue = newValue
        }
    }
}

The projected value doesn't go through this computed property, that's why it doesn't trigger the willSet. You're right that all paths access the internal storage in the end, but the willSet isn't attached to that storage.

Yes, b and c.

I suppose another option would have been to attach the observers on the stored property private var _n: Foo instead of the computed property var n: Int. That would match your expectations because mutating Foo.wrappedValue (through any code path) is also a mutation of Foo (if Foo is a value type). But then the willSet observer would fire for any mutation of Foo, not just when wrappedValue is mutated. This would probably be equally or even more surprising than the existing behavior. Regardless, I doubt the behavior can be changed now because it could break existing code.

1 Like