It did work for me without withExtendedLifetime, but as I said, you can't rely on it. Subtle changes in how you write the code or in what context it's in may change how the compiler optimizes it.
Here's my test:
$ cat runloop-timer.swift
import Combine
import Foundation
let subscription = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.sink { _ in
print("timer fired")
}
RunLoop.current.run()
$ swift --version
Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
Target: x86_64-apple-darwin19.6.0
$ swiftc -O runloop-timer.swift
$ ./runloop-timer
timer fired
timer fired
timer fired
^C
(Note that it doesn't work if I omit the subscription variable altogether and ignore the "result of call to 'sink(receiveValue:)' is unused" compiler warning.)
The compiler could certainly be implemented in such a way to never destroy objects before they go out of scope, but it was a deliberate decision to be more aggressive because doing so enables certain important optimizations.
@lukasa gives an example of this toward the end of this excellent post on the same topic: An unexpected deadlock with Combine only on Release build - #6 by lukasa
You might well ask: well then, why not make release match debug? Why not make the lifetime of all references explicitly end when their enclosing scope ends?
The easiest answer to this is that it makes Swift programs slower. Consider this code:
…