Swift regret-ish: weak vars in structs— Jordan Rose (@UINT_MIN) December 8, 2021
Currently this is the only way to have a collection of weak values without having one class per weak ref. But you have to manually filter it; it doesn’t auto-shrink when a reference goes away.
Part of the Swift Regrets series.
Swift lets you mark
vars in structs as
weak, just like in classes. Currently this is the only way to have a collection of weak values without having one class instance per weak ref. But you have to manually filter such a collection; it doesn’t auto-shrink when a reference goes away. Still, sometimes that’s what you want.
This wasn’t on my original list, but Jens Ayton brought it up because, like
lazy vars, weak vars make a containing struct act weirdly. And it makes my regret/delight tick-tock line up better. So here we go:
var obj: Optional = SomeObject() let weakWrapper = Wrapper(obj) print(weakWrapper.object == nil) // false obj = nil print(weakWrapper.object == nil) // true
There you go. The struct is declared with
let, but its member changes anyway! Value semantics advocates HATE him!
lazy, you can’t build weak references out of other existing features. A safe weak reference fundamentally needs to do some bookkeeping when initialized and destroyed. That means that if weak refs in structs weren’t allowed, you couldn’t build your own weak collection without directly talking to the runtime, whether you’re making a compacting or non-compacting one. You really want the compiler to insert the correct operations for you. We still don’t have compacting collections in the stdlib, though, so… (They’re not that easy to design or implement, but still.)
Anyway, weak refs in structs have copy/destroy overhead like strong refs, but the desired semantics make the “value” change even when “immutable”. It’d be clearer, if not necessarily better, if
WeakRef were a wrapper type with a computed property
referent. Then it’d be obvious why the value can change. But it’d be a lot more annoying to use. Still, maybe it’s a useful mental model—think of it like a property wrapper.
All that’s why most people don’t like
weak in structs, but I actually have a different reason. Swift weak references work a lot like Swift unowned references, with dedicated retain and release operations and a check on load to see if the object is alive. However, Objective-C had weak references first, and they’re very different. An Objective-C weak reference registers itself with the runtime, which then (essentially) adds an extra step to the target object’s deinitialization: “zero out this other address over here”.
That doesn’t play well with Swift structs, which can be freely copied around. Not only is the struct now forced to keep at least one field in memory rather than a register, but moving the struct, destructively, is no longer a memcpy. Every other primitive type in Swift can be moved “bitwise” / by memcpy / “trivially” in the C++ sense. Even Swift-native weak references. But AnyObject can be an Objective-C class, or a descendent thereof, and so we’re forced to consider Objective-C on Apple platforms…which means the language everywhere allows for customized, “non-trivial” moves. In practice this is mostly a code size impact, and not a huge one, but unspecialized generic code is probably a little bit slower than it otherwise needs to be because of this.
Now, this might turn out to be a good thing for C++ interop. C++ allows types to have non-trivial moves, and without that support those types would otherwise not be allowed in Swift. (Though C++ moves are non-destructive; Swift move is C++ move-then-destroy.) But I don’t know if that was worth the extra implementation complexity, even if it meant C++ interop was forever limited in Swift, or required additional special annotations for certain types.
Still, there are valid uses for weak references in structs, even if they do add implementation complexity and a bit of weirdness. If we hadn’t allowed them we would have needed to provide workarounds. And of course it’s the backwards compatibility requirement with Objective-C that got us here. So, *shrug*.