10

When I grab values from a Dictionary and put them into Array, I can't release memory any more. I tried to remove all object from Array and Dictionary, but these object still exist somewhere (deinit were not called).

I was playing in the following way:

class MyData {
    let i = 0
    init () {
        NSLog("Init")
    }
    deinit {
        NSLog("Deinit")
    }
}

var myDictionary:Dictionary<String, MyData> = ["key1":MyData(), "key2":MyData()] 
// Init was called twice

// here is the problem: extract values from Dictionary
var myValues = Array(myDictionary.values)
myValues = []  // nothing - ok, strong references are still in the dictionary

myDictionary = [:] 
// Why Deinit was not called???

If I remove these two lines for value extraction, then Deinit is called normally

var anotherDictionary:Dictionary<String, MyData> = ["key1":MyData(), "key2":MyData()]
anotherDictionary = [:] 
// Deinit is called twice

var myArray:MyData[] = [MyData(), MyData()]
myArray = []  
// Deinit is called twice

What am I doing wrong here?

How the objects should be removed in the proper way to release memory when they don't needed anymore? The problem happens only when keys or values are extracted from Dictionary (Dictionary.values or Dictionary.keys).

EDIT:

I made a workaround for this case. If I use NSDictionary instead of Dictionary and extract keys first and then take values in a for-loop, then it works.

var myDictionary:NSDictionary = ["key1":MyData(), "key2":MyData()] 
// called Init twice

var myKeys:String[] = myDictionary.allKeys as String[]
var myValues:MyData[] = []

for key in myKeys {
    myValues.append(myDictionary[key] as MyData)
}

myKeys = []
myDictionary = [:]
myValues = []
// Deinit is now called twice, but I'm not sure about keys...

But, if I use allValues instead of allKeys, then it won't work any more.

...
var anotherValues = myDictionary.allValues
anotherValues = []
myDictionary = [:]
// deinit is not called again - very scary...
Matjaz
  • 113
  • 6
  • Interesting experiment. I wonder whether it happens every time a collection is cloned, or if it's specific to dictionary keys. My guess is the former. – NRitH Jul 11 '14 at 14:42
  • I think it's a defect. Even if you put it in an autorelease pool, it doesn't call deinit. – JeremyP Jul 11 '14 at 16:11
  • Is this in a playground? – Matt Gibson Jul 12 '14 at 07:07
  • @Matt - No, it's in a project wrapped in one function. I tested also in a playground, but there deinit is never called even in a normal case (last two lines of code). – Matjaz Jul 12 '14 at 15:27
  • Fine; playgrounds tend to hang onto references as part of their debug display, so I was just making sure that wasn't confusing the issue. – Matt Gibson Jul 13 '14 at 08:51

1 Answers1

5

I don't think this is a retain cycle. I can reproduce this simply by iterating the values in the Dictionary without even introducing the Array, and without doing anything to them. I was playing with this issue when I commented out some of the code and found that this still doesn't call deinit:

    var myDictionary:Dictionary<String, MyData> = ["key1":MyData(), "key2":MyData()]
    for (_, item) in myDictionary {
        //myValues.append(item)
    }
    myDictionary = [:]
    // Why Deinit was not called???

If you take out the for loop entirely, deinit is called as expected. If you put it back in—just looping, not doing anything—then you don't get a deinit.

I'd say this is a bug in dictionary access of some kind; it's not as complicated as reference cycles between Array and Dictionary.

I can reproduce the above with this as my for loop:

for _ in myDictionary { }

...and if I take that line out, it deinits fine. That's the simplest/oddest case I could find.

So, the Array in your example is a red herring, and I think you've found a bug with dictionary access.

Matt Gibson
  • 37,886
  • 9
  • 99
  • 128