swiftautomatic-ref-countingunowned-references

When both properties can never be nil what difference does unowned make in terms of allocated memory?


For the following code from the swift programming guide with deinitializers added by me, the resulting debug printout is the same whether the unowned keyword is used or not. The swift programming guide says that using unowned and implicitly unwrapped optionals is a way to break strong reference cycles when both properties that reference each other's class instances will never be nil. If both properties will never be nil, how is that different from a strong reference cycle? For example, why do we bother using the unowned keyword in this particular circumstance, especially when the debug readout shows that memory allocations are no different whether unowned is used or not?

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
    deinit {print("\(name) is being deinitialized")}
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
    deinit {print("\(name) is being deinitialized")}
}

var canada = Country(name: "Canada", capitalName: "Ottawa")
print("\(canada.name)'s capital city is called \(canada.capitalCity.name)")
canada.capitalCity = City(name: "Vancouver", country: canada)

Debug readout:

Canada's capital city is called Ottawa
Ottawa is being deinitialized

Note: this was in a playground.


Solution

  • You're apparently looking at this in a playground or in some environment where you're not letting them fall out of scope (e.g. if they're properties of some object, look what happens when that object, itself, is deallocated). But, for illustrative purposes, consider this permutation of your code:

    func foo() {
        let canada = Country(name: "Canada", capitalName: "Ottawa")
        print("\(canada.name)'s capital city is called \(canada.capitalCity.name)")
        canada.capitalCity = City(name: "Vancouver", country: canada)
    }
    
    foo()
    

    This is like your example, but I'm constraining the scope of these variables to within the foo function, so I should be able to see the full life-cycle of objects created and destroyed within that scope.

    With unowned reference to Country in City it will report:

    Canada's capital city is called Ottawa
    Ottawa is being deinitialized
    Canada is being deinitialized
    Vancouver is being deinitialized
    

    And without unowned (and not weak, either), it will report:

    Canada's capital city is called Ottawa
    Ottawa is being deinitialized
    

    Note, the absence of any print statements associated with the deinitialization of "Canada" or "Vancouver". That's because in the absence of the unowned reference, you end up with a strong reference cycle between "Canada" and "Vancouver" and they're not deinitialized.

    So, the fact that you're seeing "Ottawa" being deinitialized has nothing to do with the strong reference cycle. That's just getting deinitialized because "Ottawa" was replaced with "Vancouver", leaving "Ottawa" with no more strong references and it is deinitialized.

    And you shouldn't draw any conclusions about the absence of any evidence of your print statements within your deinit in your original example. You could have done this in a playground, or they could have been properties of some object that, itself, has not yet been deallocated. Putting these variables within a constrained scope, such as I did with the foo function, above, better illustrates the true lifecycle of these objects as they fall out of scope. And it shows us the result of not resolving our strong reference cycles.

    Bottom line, you do need unowned (or weak) reference of Country within City to break that strong reference cycle. It's not just a question as to whether those variables could ever be set to nil, but also whether it's possible for them to ever fall out of scope and be deinitialized.