Hi Siddharth, welcome to the Swift Community!
As a first step, I recommend reading the Swift evolution proposal for property wrappers. Everything through the Detailed Design section will give you a much more in-depth understanding of property wrappers and how they work.
After that, focus on the future direction for referencing enclosing self
instance in a property wrapper. Once you've done that, continue reading the rest of this post!
I recommend getting familiar with the memory exclusivity issue outlined in the proposal by playing around with the "obvious" way to reference enclosing self
that the proposal talks about, which is registering self
with the property wrapper after self
has been fully initialized. Here's a "working" version of the problematic code shown in the proposal:
protocol Observed {
func broadcastValueWillChange<T>(newValue: T)
}
@propertyWrapper
struct Observable<Value> {
var stored: Value
var observed: Observed?
init(wrappedValue: Value) {
self.stored = wrappedValue
}
mutating func register(_ observed: Observed) {
self.observed = observed
}
public var wrappedValue: Value {
get { return stored }
set {
observed?.broadcastValueWillChange(newValue: newValue)
stored = newValue
}
}
}
class MyClass: Observed {
@Observable public var myVar: Int = 17
init() {
// self._myVar gets initialized with Observable(wrappedValue: 17) here
self._myVar.register(self) // register as an Observable
}
func broadcastValueWillChange<T>(newValue: T) {
print("oldValue: \(myVar)") // CRASH: simultaneous access to myVar!
print("newValue: \(newValue)")
}
}
let c = MyClass()
c.myVar = 10
The issue is that the compiler guards access to _myVar
for modification in the synthesized myVar
setter, which looks something like this:
private var _myVar: Observable = Observable(wrappedValue: 17)
public var myVar: Int {
get {
return _myVar.wrappedValue
}
set {
// Here, the compiler will guard access to _myVar and make
// sure nothing else accesses this memory until the set operation
// is done
_myVar.wrappedValue = newValue
}
}
The setter for _myVar.wrappedValue
in the property wrapper type calls broadcastValueWillChange
, which accesses _myVar
through the myVar
getter shown above, which triggers the exclusivity violation. The reason why the static subscript works around the issue is that it moves the access to _myVar
to the same scope where broadcastValueWillChange
is called, so the accesses are sequential rather than nested. It's important to keep this in mind when thinking about better designs for the feature, because the final design will likely still need to involve abstracting the wrappedValue
accessors as a static subscript, or on an object separate from the property wrapper instance. A few folks in the community came up with an idea to allow property wrappers opt into having static storage, so this could be one option to consider. I wrote up a description of this idea here. I recommend thinking about how that idea could be extended to support access to the enclosing self
instance.
Please let me know if you have any questions!