6

I'm trying to figure out how NSMapTable works So I'm trying in playground the following code:

class Person {
    var name: String


    init(name: String ) {
        self.name = name
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

var hobyePerson : NSMapTable? = NSMapTable<Person, NSMutableString>
(keyOptions: .weakMemory, valueOptions: .weakMemory)

var rob : Person? = Person(name: "Rob Appleseed") // print : Rob Appleseed is being initialized

hobyePerson?.setObject("golf", forKey: rob)
hobyePerson?.count // return : 1


rob = nil // print : Rob Appleseed is being deinitialized
hobyePerson?.count // return : 1 (WHY ???!!!!)

as written in the documentation: "Keys and/or values are optionally held “weakly” such that entries are removed when one of the objects is reclaimed."

why even though I initialized the object so that it has a weak reference to the key-value pair when rob is deallocated, I still have an element in hobyePerson?

emacos
  • 541
  • 1
  • 8
  • 17

3 Answers3

8

NSMapTable's weak behavior options work best when you don't care when keys/values are released, but rather, you do care that the keys/values aren't strongly retained and will be released at some point after the object of interest becomes nil.

Why so?

As a Foundation class, the authors of NSMapTable had to balance both features and performance.

Consequently, as an "optimization" for performance, they chose that weakly referenced objects that become nil are NOT immediately removed from the map table...! Rather, this happens "later" when it can be efficiently done -- such as when the map table internally gets resized, etc.

As @Luke also mentions in his answer, see this excellent writeup about an experiment done on NSMapTable's behavior for more details:

http://cocoamine.net/blog/2013/12/13/nsmaptable-and-zeroing-weak-references/

JRG-Developer
  • 12,454
  • 8
  • 55
  • 81
  • Thank you for this lead. It is great. Along 38 years of programming in the Apple garden, most of which in Obj-C and Cocoa - I found that Apple's designers of CoreFoundation and Foundation created a masterpiece. If you just "Forget about it" - and drive your way - your code will perform REALLY GOOD. Not just "mediocre". If you want to really excel - you may need to tweak a little, but no more. I wrote Audio/Video realtime codecs and effects... – Motti Shneor Dec 08 '22 at 17:45
2

Yes, this is a strange and unfortunate behavior. This article goes into it in some depth. Although it doesn't explore weak-to-weak specifically, the behavior described is the same. As that author notes, hobyePerson.keyEnumerator().allObjects.count and hobyePerson.objectEnumerator().allObjects.count will contain 0 as expected at the end of all this. He also points out that Apple has sort of documented this behavior in the Mountain Lion release notes.

However, weak-to-strong NSMapTables are not currently recommended, as the strong values for weak keys which get zero’d out do not get cleared away (and released) until/unless the map table resizes itself.

Sorry I don't have a better explanation for you.

Luke
  • 7,110
  • 6
  • 45
  • 74
  • thanks for your answer I think at this point NSMapTable has some sort of problem, because if I create a dictionary from hobyePerson: var hobyePersonDic = hobyePerson?.dictionaryRepresentation() and add this line: hobyePersonDic?.isEmpty after adding rob: hobyePerson? .setObject ("golf", forKey: rob) and after dealloc rob rob = nil I always get true. I would not know whether or not the key-value pair is actually removed. – emacos Oct 27 '17 at 22:36
0

It didn't work for me so I implemented simple weak map like this.. Will improve it overtime but for now works:

import Foundation

private struct WeakValue<Value:AnyObject> {
    weak var value: Value?
}

public class CSWeakValueDictionary<Key:AnyObject, Value:AnyObject> {
    private let dictionary = NSMutableDictionary()
    public subscript(source: Key) -> Value? {
        get {
            let value = (dictionary["\(source)"] as? WeakValue<Value>)?.value
            if value == nil { dictionary.removeObject(forKey: "\(source)") }
            return value
        }
        set { dictionary["\(source)"] = WeakValue(value: newValue) }
    }
}
Renetik
  • 5,887
  • 1
  • 47
  • 66