Swift 5.3 Programming Language Guide On Automatic Reference Counting

Hi All,
please refer to the Swift Programming Language Guide. Specifically the section on Automatic Reference Counting.

It includes an example in the Weak References section under Resolving Strong Reference Cycles Between Class Instances.

import Foundation

 class Person {
     let name: String
     init(name: String) { self.name = name }
     var apartment: Apartment?
     deinit { print("\(name) is being deinitialized") }
 }

 class Apartment {
     let unit: String
     init(unit: String) { self.unit = unit }
     weak var tenant: Person?
     deinit { print("Apartment \(unit) is being deinitialized") }
 }

 var john: Person?
 var unit4A: Apartment?

 john = Person(name: "John Appleseed")
 unit4A = Apartment(unit: "4A")

 john!.apartment = unit4A
 unit4A!.tenant = john

 john = nil
 // Prints "John Appleseed is being deinitialized"

According to the guide, when john is set to nil above, unit4A’s weak reference is also set to nil:

Because there are no more strong references to the Person instance, it’s deallocated and the tenant property is set to nil

Why then if I add this line to the end of the playground, it shows a reference to john:

 unit4A?.tenant

See the screenshot here inspecting unit4A?.tenant:

Also:

print(String(describing: unit4A?.tenant))

Shows:

    Optional(__lldb_expr_7.Person)

Please can anyone explain why this holds a reference and not nil as explained by the language guide?

I can reproduce this in an Xcode 12 beta 4 playground. Note the order of the print statements in the console. On my machine, the output looks like this:

Optional(__lldb_expr_31.Person)
John Appleseed is being deinitialized

That is, print(String(describing: unit4A?.tenant)) executes before Person.deinit, which explains why the property is not nil.

Why? ARC is free to release an object anytime between its last use and the end of its scope. The precise release point is not deterministic and can vary by compiler version or optimization level (you'll generally see that objects are released faster in release builds than in debug builds).

If you write your code like this, enclosing the lifetime of the Person object in a tighter scope, you'll see the behavior you expect:

var john: Person?
var unit4A: Apartment?

do {
  john = Person(name: "John Appleseed")
  unit4A = Apartment(unit: "4A")

  john!.apartment = unit4A
  unit4A!.tenant = john

  john = nil
  // Prints "John Appleseed is being deinitialized"
} // The Person instance will be released here at the latest

print(String(describing: unit4A?.tenant))
3 Likes

Makes sense. Thanks Ole!

Curiously, printing unit4A?.tenant twice, the second time shows nil. Searching this topic, also found some interesting articles by Mike Ash. See Friday Q&A 2017-09-22: Swift 4 Weak References.