简体   繁体   中英

Could a guard let `self` = self inside a closure that uses [weak self] cause a crash if the object they reference becomes deallocated?

I've looked at some comments to questions on stack overflow about using [weak self] and [unowned self]. I need to be sure I understand correctly.

I am using the latest Xcode - Xcode 13.4, the latest macOS - macOS Monterey 12.4, and I'm writing code compatible with iOS 12.0 to the latest iOS 15.

First of all, am I correct that memory that has a strong reference to it is owned by whichever the strong reference belongs to?

Also, and this is what I really need to know, I want to know what happens if I use [weak self] in at the beginning of a closure, and then I have the statement guard let `self` = self else { return } . I believe the guard statement, if the condition succeeds, assigns the reference self holds to the newly declared strong reference named self in the let `self part of the guard statement. Is that correct?

What that leads me to ask is, What happens at the end of the closure. Am I correct that even though the newer self holds a strong reference, the memory the strong reference points to is deallocated once the code in the closure has executed the last statement in the closure, because the newer self with the strong reference is declared within the closure itself, and is deallocated along with all memory that had been allocated for that closure.

I think I got all of this right. If not, please let me know.

I am interested in any additional information or any clarifications if you will.

If the closure holds a strong reference, it prevents the ARC to release the object as long as the closure is still accessible.

The weak and unowned self allow to resolve this strong ownership .
But this means that the object referenced by the closure's weak self could be deinitailized while the closure could still be called. In this case, the closure's self would refer to nil.

The guard would in fact just check that the reference is still valid. A nil would make it fail anyway. If it's a valid reference, it doesn't change the capture list; you could just assume that self will stay valid until the current execution of the closure returns.

Experimental proof

Here some code to experiment the difference, with a class that traces initialisation and deinitialization, and that can return a closure with self captured:

class T {
    var id:String
    init(_ name:String){
        id = name
        print ("INIT T object \(id)")
    }
    func getClosure() -> (Int)->Int {
        return { [weak self] (i:Int) in    
            guard let self = self else {
                print ("Instance vanished")
                return -1
            }
            print (i,self.id)
            return 0
        }
    }
    deinit {
        print ("DEINIT T object \(id)")
    }
}

Let's create the dramatic circumstances where the closure could survive the instance that created it:

var oops:(Int)->Int = {(Int)->Int in 0}

func f1() {
    var t = T("f1_test")
    t.getClosure()(32)       // closure is disposed at the end of the function
}                            // t will be deinitialized in any case
func f2() {
    var t = T("f2_test")
    oops = t.getClosure()    // closure will be kept outside of scope
}                            // t can be disposed only if capture is not strong

f1()
f2()
oops(33)

Let's analyse what happens. The f1() will result in a T instance that will be deinitialised. This is because the closure is no longer referenced when leaving f1 , so the captured self is no longer referenced either, and the ARC will terminate it:

INIT T object f1_test
32 f1_test
DEINIT T object f1_test

The f2() call is more subtle, because the T instance is still referenced by the closure when leaving f2 , and the closure survives in a global variable.

If the capture would be strong, you wouldn't need the guard , since ARC would make sure that the instance is still available. You'd see that the T instance would not be deinitialized.

But since the capture is weak , there is no strong reference to the T instance when returning from f2 and it would be deinitialized properly. So the guard would cause your to get:

INIT T object f2_test
DEINIT T object f2_test
Instance vanished

This proves that the guard let self = self does not replace the closure's weak capture with a strong one.

When you write a closure like this:

{ [weak self] in
    guard let self = self else { return }
    ...
}

You are checking that the class in which this closure is declared is still allocated, if that class is still allocated you are creating a new strong reference to it inside the closure itself.

You do that because if all other strong reference to the class are deleted there will still be the strong reference created in

guard let self = self else { return }

At that point you can be sure that until the end of the closure the class in which the closure is declared is allocated, because the reference is in the closure itself.

So, to answer your question, no, if you write

guard let self = self else { return }

that closure can't crash because you have a strong reference that's still alive.

Different thing is if you use [unowned self]. Unowned self is like an unwrapped optional. If you use it without checking its existence you are implicitly writing

self!.<something>

so if the self is deallocated when you call that closure it will crash.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM