object lifetimes in swift

As a swift noob, and coming from C++, I wonder why the following prints 'BOOM!!':

var text = ""
class ArrayHandle {
    var array = Array()
    init() {
        text = "success"
    }
    deinit {
        text = "BOOM!!"
    }
}
struct Array { var buffer = Buffer() }
struct Buffer { func doStuff() { print(text)} }
var buffer = ArrayHandle().array.buffer.doStuff()

Apparently the deinit is called before the text is printed...

When you change the code to:

var handle = ArrayHandle()
handle.array.buffer.doStuff()

it prints the expected 'success'

Is there a way to force developers to use a var to store an object, or is that just a hidden bug that may fail in the future?

Answered by Claude31 in 711293022

If there is no exact control over the lifetime of an object in swift

There is.

When you declare a var or const, it remains referenced (and counted in ARC) as long as the block of its scope remains loaded.

For instance:

let handle = ArrayHandle()

if it is defined in a func, it stays referenced as long as you did not exit the func.

In a closure, as long you have not exited the closure (unless you declare the closure as escaping, then it survives after leaving.

In an if statement, as long as you are in the if statement.

Really interesting.

My understanding: when you call

var buffer = ArrayHandle().array.buffer.doStuff()

or just

ArrayHandle().array.buffer.doStuff()

ArrayHandle() instance is no more needed once you've got a reference to array. So compiler may continue with array.buffer with no need of the instance.

As there is no other reference to this ArrayHandle() instance, it can be deallocated and deinit is called automatically.

But if you declare a var handle, then it cannot be deallocated by ARC and thus deinit is not called at this point.

That's detailed in swift language Deinitializers in Action. But not with the detail on what happens in a sequence of references like here.

C++ has no ARC mechanism. I guess the behaviour comes from ARC in Swift.

I did a few more tests:

Just ArrayHandle class:

class ArrayHandle {
    init() {
        print("init")
        text = "success"
    }
    deinit {
        print("deinit")
        text = "BOOM!!"
    }
    func doStuff() {  // Moved inside
        print(text)
    }
}
ArrayHandle().doStuff()

You get the expected log:

  • init
  • success
  • deinit

Explanation: To call doStuff(), compiler needs to keep ArrayHandle(), it cannot deallocate ArrayHandle() yet. Hence, ARC cannot deinit.

Keep Buffer only:

struct Buffer {
    func doStuff() {
        print(text)
    }
}

class ArrayHandle {
    var buffer = Buffer()
    init() {
        print("init")
        text = "success"
    }
    deinit {
        print("deinit")
        text = "BOOM!!"
    }
}
ArrayHandle().buffer.doStuff()

You get the BOOM:

  • init
  • deinit
  • BOOM!!

Here, I understand that compiler loads an ArrayHandle instance, aka temporary.

let handle = ArrayHandle()

But does not create this permanent reference (just as if not counting in ARC counter)

Then it loads buffer, aka

let buffer = ArrayHandle().buffer

At this stage, temporary handle is no more referenced anywhere (we have not declared a var, it is just temporary by the compiler). Hence ARC can deallocate immediately and deinit is called. Then doStuff() is called on buffer, after deinit.

For what it's worth. Maybe some deep expert in Swift may confirm or deny this simplistic analysis ?

Accepted Answer

If there is no exact control over the lifetime of an object in swift

There is.

When you declare a var or const, it remains referenced (and counted in ARC) as long as the block of its scope remains loaded.

For instance:

let handle = ArrayHandle()

if it is defined in a func, it stays referenced as long as you did not exit the func.

In a closure, as long you have not exited the closure (unless you declare the closure as escaping, then it survives after leaving.

In an if statement, as long as you are in the if statement.

object lifetimes in swift
 
 
Q