This is about using self?.f1() in closures vs using guard let self and then using self.f1()
Note: Apologies if this question has been asked many times but I keep getting different opinions on this.
Option 1:
Personally I prefer not to use guard let self in closures as I feel it captures self strongly. Instead I just encapsulate the logic I want to perform into a function for example processData and call that function as self?.processData(data)
Option 2:
I was told if I used self?.processData there is a chance self might get deallocated midway and so it is better to use guard let self and use self.processData(data)
Question
Is it better to use Option 1 or Option 2? and Why?
Is what I was told about self getting deallocated midway while self?.processData(data) is executed correct or is a misconception?
Code
final class A: Sendable {
func fetchData() {
// Just a sample request
let request = URLRequest(url: URL(string: "")!)
let session = URLSession(configuration: .ephemeral)
session.dataTask(with: request) { [weak self] data, response, error in
// Option 1
self?.processData(data)
// Option 2
guard let self else {
return
}
self.fetchData()
}
}
private func processData(_ data: Data?) {
// do something here
}
}
guard let self does capture self strongly, but only for the duration of the closure. This is a good thing because what you heard about self being de-allocated between the null check and the method call is true. Always make sure you get a strong reference before you attempt to call methods or access properties on a weak binding.
If you only need to access the weak reference for a small part of your closure and want to drop the strong reference before the closure is finished, you could always wrap your method calls inside an if let self instead of using guard. I doubt that's the common case though.
You might also be able to use Optional.map and friends, but I'm not 100% sure how that interacts with weak references.
My understanding is that self isn't supposed to be able to be deallocated while you're mid-method, because the method requires a strong reference. Where things get hairy is if you need to call more than one method on the weak reference:
With option 1, self is promoted to a strong reference for the whole closure. With option 2, self is promoted to a strong reference temporarily for foo, and then again for bar. This is not only potentially less efficient, but leaves room for self to be deallocated between foo and bar, AIUI.
I could be totally wrong but based on my testing if self?.foo() started then self?.bar would also run.
I did a small example and ran it macOS command line project.
Code
import Foundation
class Service {
init() {
print("Service init")
}
func fetchData(completion: () -> ()) {
for _ in 1...20_000_000 {}
completion()
}
deinit {
print("Service deinit")
}
}
class A {
let service: Service
init(service: Service) {
print("A init")
self.service = service
}
func updateData() {
service.fetchData { [weak self] in
self?.f1()
self?.f2()
}
}
func f1() {
print("f1 started")
for _ in 1...20_000_000 {}
print("f1 ended")
}
func f2() {
print("f2 started")
for _ in 1...20_000_000 {}
print("f2 ended")
}
deinit {
print("A deinit")
}
}
let service = Service()
do {
let a = A(service: service)
Task {
a.updateData()
}
print("last line of do")
}
print("outside")
RunLoop.main.run()
Output
Service init
A init
last line of do
outside
f1 started
f1 ended
f2 started
f2 ended
A deinit
There are two ways that it could become nil between the foo() and bar() calls. One is a race condition in a multithreaded environment, where one thread releases the last strong reference as another thread calls the closure. The easier one to demonstrate is that foo() itself could cause the last strong reference to be released:
var globalA: A?
class A {
func foo() {
print("In foo()")
globalA = nil
}
func bar() {
print("In bar()")
}
deinit {
print("A deinit")
}
}
do {
let a = A()
globalA = a
Task { [weak a] in
try? await Task.sleep(nanoseconds: 1_000_000_000)
a?.foo()
a?.bar()
}
}
try! await Task.sleep(nanoseconds: 2_000_000_000)
/* Output:
In foo()
A deinit
*/