11

I added a variable of Timer to my class, and used its didSet observer to invalidate old value

var timer: Timer? {
    didSet { oldValue?.invalidate() }
}

deinit {
    timer = nil
}

I thought this would be enough to invalidate timer when class is deinitialized, but looks like didSet is not called. Why is that? Are observers not working during deinitialization?

mfaani
  • 33,269
  • 19
  • 164
  • 293
Damian Dudycz
  • 2,622
  • 19
  • 38
  • 4
    Very good observation! Clearly you're right, and it makes sense (we don't want to trigger side effects during deinitialization), but I don't see it documented clearly anywhere. – matt Oct 15 '18 at 16:09
  • 2
    Not an answer to your question. But just to avoid confusion and ease the reading it's best to just call `timer.invalidate()` (so that the run loop would remove its pointer), then upon deallocation of the object itself the timer will also get removed. I mean between setting your timer to `nil` and calling `invalidate` on it it, the invalidation is 10X more important to do... – mfaani Oct 15 '18 at 16:22
  • 3
    I've filed a bug because it turns out you _can_ work around this by using `defer`, which should be impossible. – matt Oct 15 '18 at 16:26
  • @Honey That's a very wise observation! – matt Oct 15 '18 at 16:32
  • This is also the case in initialisers, property accesses are done "direct to storage", i.e bypassing observers, and you can evade the semantic analysis that determines this by wrapping the access in a `defer` block or a closure. More generally, you can evade this analysis by calling off to a method which can do arbitrary non-direct accesses. But yeah, arguably `defer` blocks in initialisers and deinitialisers should perform direct accesses. – Hamish Oct 15 '18 at 17:52
  • @matt just to be clear, you meant wrapping `timer = nil` inside `defer` which is all already inside `deinit` works (while it shouldn't)? – mfaani Oct 15 '18 at 17:53
  • @Honey https://bugs.swift.org/browse/SR-9000 – matt Oct 15 '18 at 17:57
  • 1
    @Hamish "and you can evade the semantic analysis" Right, I'm suggesting you shouldn't be able to. But I don't think the cases are identical. Postponing something to after initialization is coherent, because we are now initialized. Postponing something to after deinitialization is just nuts. – matt Oct 15 '18 at 18:02
  • 2
    @matt "Postponing something to after initialization is coherent, because we are now initialized" – sure, I agree that it makes perfect sense, but the current direct to storage logic doesn't differentiate between "not yet initialised" and "initialised", e.g https://gist.github.com/hamishknight/4af8509b0eaf889ff763770949e8920a. Arguably this model is simpler and easier to reason about than a model where the second access is non-direct. But I do think that `defer` should align with this model, i.e it should have the same access semantics as the second access in my example. – Hamish Oct 15 '18 at 18:12
  • @Hamish Brilliant clarification, thanks. – matt Oct 15 '18 at 18:13
  • 1
    "Postponing something to after deinitialization is just nuts" – A `defer` in a `deinit` isn't postponing logic until *after* the `deinit` has returned, it's deferring the logic until just before the `deinit` returns. – Hamish Oct 15 '18 at 18:13
  • @Hamish I don't know that, I don't believe it, and I don't know how you would prove it. This example seems to prove just the opposite: https://stackoverflow.com/a/36185126/341994 – matt Oct 15 '18 at 18:15
  • 1
    @matt That's [how it's documented](https://docs.swift.org/swift-book/ReferenceManual/Statements.html#grammar_defer-statement) :) "*A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.*". It will run after the evaluation of the `return`'s operand (for lack of a better name), though, which explains your example. – Hamish Oct 15 '18 at 18:18
  • Very related: [NSTimer memory management](https://stackoverflow.com/questions/13534654/nstimer-memory-management) – mfaani Oct 29 '18 at 15:37

2 Answers2

11

Let's put an answer here so we can close this off.

  • It seems that property observers apparently do not run during deinit. This seems parallel to the fact that property observers do not run during init, but unlike the latter, the former doesn't seem to be clearly documented anywhere.

  • You can work around this by semantic trickery, but don't! That seems like a bug (and I've filed it).

  • The original use case was not a very good one to begin with. Hiding invalidation of a timer in replacement sounds like a potential maintenance nightmare. Agreed that invalidation and replacement go together like ham and eggs, what I always do is write a method that invalidates and replaces, in that order, and funnel everything through that method. (This can be enforced if necessary, but I won't go into that.) That method can be called during deinit.

SUPPLEMENTARY NOTE: Watch out, when using a timer, for memory management issues! You can easily get yourself into a situation where deinit is never called, because you're retaining the timer but the timer is retaining you. You will then fail to invalidate the timer and your entire view controller will leak. You don't complain about that in your question, but it's a related matter so I thought I'd better flag it.

matt
  • 515,959
  • 87
  • 875
  • 1,141
0

call like this, if you want the setters to trigger

deinit {
    { timer = nil }()
}
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179