0

I came across something that's peculiar and interesting and would love to get inputs from anyone. So to start off with lets take this definition of the class:

class TestClass:NSObject {  
    var s1 = NSHashTable<TestClass>(options: .weakMemory)  

    func doit(bla:TestClass) {  
        s1.add(bla)  
        bla.s1.add(self)  
    }  

    deinit {  
        print("Deinit")  
    }  
}  

Now lets consider the following:

var t1:TestClass? = TestClass()  
var t2:TestClass? = TestClass()  

If we did the following deinit gets called:

t1?.s1.add(t2!)  
t2?.s1.add(t1!)  

t1 = nil // This will result in deinit being called  

Now lets do the same thing but by calling doit() method

t1?.doit(bla:t2!)  

t1 = nil // Deinit doesn't get called for some reason  

The question here is why isn't deinit being called in this situation? What is so different about this since it essentially has the same reference assignment as the first method?

I would love to get input from anyone on this.

  • How have you tested your code? The line `t1?.doit(t2!)` does not compile in Swift 3. As far as I tested, both codes (`t1?.doit(t2!)` fixed as `t1?.doit(bla: t2!)`) have shown **`Deinit`** at the line `t1 = nil`. (Xcode 8.2.1, Command Line Tool project) – OOPer Mar 02 '17 at 11:50
  • I cannot reproduce this in either Swift 3.0.2 or Swift 3.1. – Hamish Mar 02 '17 at 13:15
  • Sorry corrected this code – user6442461 Mar 02 '17 at 19:02
  • @OOPer I ran the `doit()` method call on my playground (same xcode version) and `Deinit` doesn't get printed at all. It only printed with the first method – user6442461 Mar 02 '17 at 19:08

2 Answers2

1

As usual, the problem is that you are trying to test this in a playground. Don't. Playgrounds are the work of the devil.

Test in an actual app project and you will see that deinit is called.

Example (iOS, but the equivalent in macOS would do fine):

import UIKit

class TestClass:NSObject {
    var s1 = NSHashTable<TestClass>(options: .weakMemory)
    func doit(bla:TestClass) {
        s1.add(bla)
        bla.s1.add(self)
    }
    deinit {
        print("Deinit")
    }
}

class ViewController: UIViewController {
    var t1:TestClass? = TestClass()
    var t2:TestClass? = TestClass()

    override func viewDidLoad() {
        super.viewDidLoad()
        t1?.doit(bla:t2!)
        t1 = nil // --> "Deinit"
        print(t2?.s1) // --> it's empty
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

deinit is not called because you've created reference cycle.

First you're creating sctrong reference from self to bla: s1.add(bla)

Second you create strong reference from bla to self: bla.s1.add(self)

And now they both have references to each other, so they won't deinit if you just nullify one of them.

I've modified your TestClass to remove reference cycle:

class TestClass:NSObject {
  weak var s1 = NSHashTable<TestClass>(options: .weakMemory)

  func doit(bla:TestClass) {
    s1?.add(bla)
    bla.s1?.add(self)
  }

  deinit {
    print("Deinit")
  }
}

Now your second call will trigger deinit properly.

NSDmitry
  • 460
  • 5
  • 12
  • There is no reference cycle, as `NSHashTable` holds weak references to the objects added. – Hamish Mar 02 '17 at 13:17
  • You can test it yourself: `t1?.doit(t2!) print("t1 = \(t1!)") t1 = nil print(t2!.s1.allObjects) // here you will see t1 address` – NSDmitry Mar 02 '17 at 13:49
  • Nope, I just get `[]` printed, as I would expect, as the hash table holds weak references to the objects added when `.weakMemory` is provided for the `options:` (feel free to [check the documentation](https://developer.apple.com/reference/foundation/nspointerfunctions.options/1415896-weakmemory)). – Hamish Mar 02 '17 at 13:55
  • I see the options, but for me, it's still there even after I do `t1 = nil` – NSDmitry Mar 02 '17 at 14:10
  • With regards to @Hamish, that's the point of setting s1 to `.weakMemory` so that the reference held are weak relationships. Because they are weakly held `t1 = nil` should be able to be released – user6442461 Mar 02 '17 at 18:56