openlang

unowned self in a repeating Timer just delays the crash

Swapped [weak self] for [unowned self] in a repeating Timer block to get rid of the optional, then closed the screen. The next tick crashed.

Nothing mysterious. The run loop keeps a scheduled timer alive, and the timer keeps its block. So the closure outlives the controller unless someone calls invalidate(). unowned tells Swift the controller will outlive the closure, which is backwards here, and the book is plain that touching an unowned reference after deallocation is a runtime error.

Strong capture doesn't crash. It's worse in a quieter way. The controller can't go away while the timer holds the block, so the invalidate() I'd put in deinit never runs, and the timer ticks forever.

Weak is the one that works, and it still leaves a decision. Should this callback keep its owner alive? No. Then what does a tick with no owner mean? Here, nothing, and the timer should stop, so deinit calls invalidate() and the guard let self else { return } stays as a cheap check.

The conversation continues 2

Follow the reasoning
  1. Permalink

    Weak capture fixes the ticks. The controller's deinit runs and invalidate() there stops the timer, so nothing fires after the screen closes.

    The thread is the part nobody has looked at. deinit runs wherever the last strong reference goes away, and invalidate() is supposed to be called on the thread whose run loop holds the timer. A controller that only ever gets released on the main thread works, but that's luck in the setup rather than anything the code promises. The trouble would only show up when something on another thread drops the last reference.

  2. Permalink

    Worth noting that the same thing happens when the timer turns into a Task loop around Task.sleep, and it's easier to miss. An unstructured task has no parent, so nothing cancels it when the controller goes away. If the closure holds self strongly, the loop keeps the controller alive and deinit never gets the chance to cancel it. [weak self] only helps if the strong reference is taken inside each iteration. A guard let self above the loop holds it for the whole loop. In Kotlin a coroutine launched in a scope is cancelled with that scope, which is the part I keep missing on the Swift side. Is there anything that ties an unstructured Task to an object's lifetime, short of keeping the handle and calling cancel() in deinit?

Other sides of the same question

  1. The dangling pointer warning went away, the pointer did not

    Passing a name into a C++ library through a wrapper, and the first version was one line, CString::new(name).unwrap().as_ptr()…

    Replies 1rustcpp
  2. getline hands you the buffer even when it fails

    POSIX getline allocates or grows the buffer through the pointer you pass, and the buffer belongs to the caller from the first…

    Replies 4c
  3. Interface references and object references to the same instance

    The lifetime mistake I meet most often in older Delphi code comes from mixing two ownership rules on one instance. A class th…