How do you hold a collection of objects without owning them: WeakBox arrays, NSHashTable and NSMapTable?
Predict first
How many entries survive, and which counts are stale?
import Foundation
final class Obs: NSObject { let n: Int; init(_ n: Int) { self.n = n }; deinit { print("deinit \(n)") } }
struct WeakBox<T: AnyObject> { weak var value: T? }
let hashTable = NSHashTable<Obs>.weakObjects()
let mapTable = NSMapTable<NSString, Obs>.strongToWeakObjects()
var boxes: [WeakBox<Obs>] = []
func fill() {
let a = Obs(1), b = Obs(2)
boxes = [WeakBox(value: a), WeakBox(value: b)]
hashTable.add(a); hashTable.add(b)
mapTable.setObject(a, forKey: "a")
}
autoreleasepool { fill() }
print("boxes: \(boxes.count) entries, \(boxes.filter { $0.value == nil }.count) nil")
print("table.count \(hashTable.count), allObjects \(hashTable.allObjects.count)")
print("map.count \(mapTable.count), keys \(mapTable.keyEnumerator().allObjects.count)")
boxes.removeAll { $0.value == nil }
print("compacted boxes: \(boxes.count)")
- deinit 2, deinit 1, boxes: 2 entries, 2 nil / table.count 2, allObjects 0 / map.count 1, keys 0 / compacted boxes: 0
- All counts drop to 0 immediately
- The objects stay alive because the tables are strong
Mental model. A weak list is a guest book in a hotel lobby: people can sign it without being forced to stay, and the book keeps names of people who left until someone tidies it.
Swift collections hold strong references, so an observer list or a registry of views keeps everything alive. Three ways to hold weakly: an array of wrapper structs with a weak var (WeakBox), NSHashTable.weakObjects() (a set) and NSMapTable with weak keys or values. In the check all three stop owning the two objects, which die at once (deinit 2, deinit 1). The difference is what you see afterwards: the WeakBox array keeps its two empty entries (2 entries, 2 nil) until you compact it, and you filter or remove nil boxes yourself. NSHashTable and NSMapTable drop dead entries lazily, so their count can still report the stale number (2 and 1) while allObjects or the key enumerator show the truth (0).
So: never use count of those tables as a liveness measure, iterate a snapshot. They also work only for class types, are not thread-safe, and the table types compare with isEqual/hash unless you ask for pointer identity (NSPointerFunctions). The check wraps the filling in autoreleasepool because Foundation may hold an autoreleased reference for a while: without the pool the objects can stay alive until the surrounding pool drains, which is exactly the hidden-retain effect of Q157. For a notification-like list prefer an explicit add/remove API with tokens, or Combine/NotificationCenter, rather than a hand-rolled weak list.
Weak holders do not own the objects; the wrapper array keeps empty slots and the Foundation tables report stale counts.
Common traps
- Reading
NSHashTable.countto know how many are alive. It can include entries whose object is gone. - Forgetting to compact a WeakBox array. It grows with dead boxes for the life of the owner.
They might follow up with
- Is weak storage thread-safe? No; guard it with a lock or an actor.
- Can a struct be weakly held? No; weak applies to class instances only.
Related questions
- 3.2.2 Why is a weak reference optional, what can be weak, and what is the trap when reading one?
- 3.6.4 What hidden retains and autoreleases does calling Objective-C add?
A closure captures [weak self] and promotes it with guard let. Can a… →