iosswiftnsmutabledictionarynscachenscopying

Does the different way of handling key between NSMutableDictionary and NSCache (copy vs. retain) result in different consequence?


I studied the differences between NSMutableDictionary and NSCache.

One of them was that NSMutableDictionary copy its keys and NSCache just retain it as a strong reference.

In addition, I found that the reason why the key is copied from the NSMutableDictionary is that if the key is only retained, a strange situation occurs when setting another value after changing the key value.

        let mutableDic = NSMutableDictionary()
        var dicKey: NSString = "key"
        mutableDic.setObject("one", forKey: dicKey)
        dicKey = "changedKey"
        mutableDic.setObject("two", forKey: dicKey)

        print(mutableDic.object(forKey: "key") ?? "") //"one"
        print(mutableDic.object(forKey: "changedKey") ?? "") //"two"

But when I performed the same action using NSCache I felt something strange. Because it worked fine even though it does not copy its keys!

        let cache = NSCache<NSString, NSString>()
        var cacheKey: NSString = "key"
        cache.setObject("one", forKey: cacheKey)
        cacheKey = "changedKey"
        cache.setObject("two", forKey: cacheKey)
        print(cache.object(forKey: "key") ?? "") //"one"
        print(cache.object(forKey: "changedKey") ?? "") //"two"

So I wonder, after all what the different results will be from copying and retaining.

Is there any advantage to retain keys rather than copy except no need to implement the NScopying protocol as a key? or vice versa?

Can you explain with examples?


Solution

  • The key here (ha) is in the assignment to dicKey/cacheKey. Specifically, the assignments

    dicKey = "changedKey"
    cacheKey = "changedKey"
    

    are not changing the value of the original dicKey and cacheKey instances, but creating new string objects and setting the local variables to point to these new objects.


    In the dictionary case:

    1. dicKey points to an object K₁ whose value is "key"
    2. mutableDic.setObject("one", forKey: dicKey) copies dicKey into a new key object K₂; K₁ is left alone
    3. dicKey = "changedKey" creates a new object K₃ with the value "changedKey", and assigns dicKey to point to it
      • Since nothing points to K₁ anymore, its reference count goes to 0 and the object is deallocated
    4. mutableDic.setObject("two", forKey: dicKey) copies dicKey into a new key object K₄; K₂ is left alone

    The end result is that the dictionary contains K₂ and K₄, while dicKey points to K₃.


    In the cache case:

    1. dicKey points to an object K₁ whose value is "key"
    2. cache.setObject("one", forKey: cacheKey) retains K₁ for insertion into the cache
    3. cacheKey = "changedKey" creates a new object K₂ with a value "changedKey" and assigns cacheKey to point to it
      • Since the cache is still retaining K₁, it remains alive and in memory, even if dicKey no longer points to it
    4. cache.setObject("two", forKey: cacheKey) retains K₂ for insertion into the cache

    The end result is that the cache contains K₁ and K₂, and cacheKey points to K₂.


    If instead of being an NSString, dicKey and cacheKey were NSMutableString, whose value can be modified at runtime without creating a new object, you'd see different behavior in the cache case:

    let mutableDic = NSMutableDictionary()
    var dicKey: NSMutableString = "key" // K₁
    mutableDic.setObject("one", forKey: dicKey) // K₂
    dicKey.setString("changedKey") // still K₁
    mutableDic.setObject("two", forKey: dicKey) // K₃
    
    print(mutableDic.object(forKey: "key") ?? "") // "one"
    print(mutableDic.object(forKey: "changedKey") ?? "") // "two"
    
    // BUT:
    
    let cache = NSCache<NSString, NSString>()
    var cacheKey: NSMutableString = "key" // K₁
    cache.setObject("one", forKey: cacheKey) // still K₁
    cacheKey.setString("changedKey") // still K₁
    cache.setObject("two", forKey: cacheKey) // still K₁!
    
    print(cache.object(forKey: "key") ?? "") // "" !!!
    print(cache.object(forKey: "changedKey") ?? "") // "two"