📜 ⬆️ ⬇️

Swift capture lists: what is the difference between weak, strong and unowned links?


Joseph Wright, "Captive" - Illustration of "Strong" Capture

The list of "captured" values ​​is in front of the list of closure parameters and can "capture" values ​​from the scope in three different ways: using the links "strong", "weak" or "unowned". We often use it mainly to avoid cycles of strong links (“strong reference cycles” aka “retain cycles”).
It’s difficult for a beginner developer to decide which method to apply, so you can spend a lot of time choosing between “strong” and “weak” or between “weak” and “unowned”, but over time, you will realize that the right choice - only one.

First, create a simple class:

class Singer { func playSong() { print("Shake it off!") } } 

Then we write a function that creates an instance of the Singer class and returns a closure that calls the Singer class's playSong () method:
')
 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

Finally, we can call sing () anywhere to get the result of executing playSong ()

 let singFunction = sing() singFunction() 


As a result, the string “Shake it off!” Will be displayed.

"Strong" capture (strong capturing)


Until you explicitly indicate how to capture, Swift uses a “strong” capture. This means that the closure captures the used external values ​​and never allows them to free themselves.

Let's look at the sing () function again.

 func sing() -> () -> Void { let taylor = Singer() let singing = { taylor.playSong() return } return singing } 

The taylor constant is defined inside the function, so that, under normal circumstances, its place would be freed as soon as the function finished its work. However, this constant is used inside the closure, which means that Swift will automatically ensure its presence as long as the closure itself exists, even after the function ends.
This is a "strong" grip in action. If Swift allowed taylor to be released, then a closure call would be insecure - its taylor.playSong () method is no longer valid.

"Weak" capture (weak capturing)


Swift allows us to create a “ capture list ” to determine exactly how the values ​​used are captured. The alternative to “strong” capture is “weak” and its use leads to the following consequences:

1. "Weak" captured values ​​are not held by the closure and, thus, they can be released and set to nil .

2. As a consequence of the first item, “weakly” captured values ​​in Swift are always optional .
We modify our example using “weak” capture and immediately see the difference.

 func sing() -> () -> Void { let taylor = Singer() let singing = { [weak taylor] in taylor?.playSong() return } return singing } 

[weak taylor] - this is our “ capture list ”, a special part of the closure syntax, in which we give instructions on exactly how values ​​should be captured. Here we say that taylor should be captured “weakly”, so we need to use taylor? .PlaySong () - now this is optional , because it can be set to nil at any time.

If you now execute this code, you will see that calling singFunction () no longer causes the message to be output. The reason for this is that taylor exists only inside sing () , and the closure returned by this function does not hold taylor "strongly" inside itself.

Now try changing taylor? .PlaySong () to taylor! .PlaySong () . This will lead to the forced unpacking of the taylor inside the circuit, and, accordingly, to a fatal error (unpacking the contents containing nil )

"Unattended" capture (unowned capturing)


An alternative to "weak" capture is "ownerless."

 func sing() -> () -> Void { let taylor = Singer() let singing = { [unowned taylor] in taylor.playSong() return } return singing } 

This code will end up abnormally similar to the forcibly deployed optional given a little higher - unowned taylor says: "I know for sure that taylor will exist for the entire closure life time, so I don’t need to keep it in memory." In fact, taylor will be released almost immediately and this code will crash.

So use unowned with extreme caution.

Frequent possible problems


There are four problems developers face when using value capture in closures:

1. Difficulties with the location of the capture list in the case where the closure takes parameters


This is a common difficulty that can be encountered at the beginning of the study of closures, but, fortunately, in this case Swift will help us.

When using the capture list and closure parameters together, the capture list is first in square brackets, then the closure parameters, then the in keyword, which marks the beginning of the closure “body”.

 writeToLog { [weak self] user, message in self?.addToLog("\(user) triggered event: \(message)") } 

Attempting to put a capture list after the closure parameters will result in a compilation error.

2. The emergence of a cycle of strong links, leading to a memory leak


When entity A has an entity B and vice versa, you have a situation called the strong reference cycle (“retain cycle”).

As an example, consider the code:

 class House { var ownerDetails: (() -> Void)? func printDetails() { print("This is a great house.") } deinit { print("I'm being demolished!") } } 

We have defined the class House , which contains one property (closure), one method and a de-initializer, which will display a message when the class instance is destroyed.

Now we will create a class Owner , similar to the previous one, except that its closure property contains information about the house.

 class Owner { var houseDetails: (() -> Void)? func printDetails() { print("I own a house.") } deinit { print("I'm dying!") } } 

Now create instances of these classes inside the do block. We do not need a catch block, but using a do block will ensure the destruction of instances immediately after}

 print("Creating a house and an owner") do { let house = House() let owner = Owner() } print("Done") 

As a result, the following messages will be displayed: “Creating a house and an owner”, “I'm dying!”, “I'm being demolished!”, Then “Done” - everything works as it should.

Now create a strong link loop.

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = owner.printDetails owner.houseDetails = house.printDetails } print("Done") 

Now the messages “Creating a house and an owner”, then “Done” appear. De-initiators will not be called.

This is due to the fact that the house has a property that points to the owner, and the owner has a property that points to the house. Therefore, none of them can be safely released. In a real situation, this leads to memory leaks, which lead to a decrease in performance and even to an abnormal termination of the application.

To remedy the situation, we need to create a new closure and use a “weak” hold in one or two cases, like this:

 print("Creating a house and an owner") do { let house = House() let owner = Owner() house.ownerDetails = { [weak owner] in owner?.printDetails() } owner.houseDetails = { [weak house] in house?.printDetails() } } print("Done") 

There is no need to declare both values ​​captured, it is enough to do it in one place - this will allow Swift to destroy both classes when necessary.

In real projects, a situation of such an obvious cycle of strong references rarely arises, but this all the more speaks of the importance of using “weak” capture with proper development.

3. Unintentional use of "strong" links, usually when capturing multiple values


Swift by default uses “strong” capture, which can lead to unintended behavior.
Consider the following code:

 func sing() -> () -> Void { let taylor = Singer() let adele = Singer() let singing = { [unowned taylor, adele] in taylor.playSong() adele.playSong() return } return singing } 

Now we have two values ​​captured by a closure, and we use both of them in the same way. However, only taylor is captured as unowned — adele is captured strongly, because the unowned keyword must be used for each value captured.

If you did it intentionally, then everything is fine, but if you want both values ​​to be captured " unowned ", you need the following:

 [unowned taylor, unowned adele] 

4. Copying closures and separating captured values


The last case on which developers stumble is how closures are copied, because the data they captured becomes available to all copies of the closure.
Consider an example of a simple closure that captures the integer variable numberOfLinesLogged declared outside the closure, so that we can increase its value and print it every time the closure is called:

 var numberOfLinesLogged = 0 let logger1 = { numberOfLinesLogged += 1 print("Lines logged: \(numberOfLinesLogged)") } logger1() 

This will display “Lines logged: 1”.
Now we will create a copy of the closure that will share the captured values ​​along with the first closure. Thus, we call the original closure or its copy, we will see the growing value of the variable.

 let logger2 = logger1 logger2() logger1() logger2() 

This will display “Lines logged: 1” ... “Lines logged: 4” because logger1 and logger2 indicate the same captured variable numberOfLinesLogged .

When to use “strong” grip, “weak” and “ownerless”


Now, when we understand how everything works, let's try to summarize:

1. If you are sure that the captured value will never become nil when performing a closure, you can use “unowned capturing” . This is an infrequent situation where the use of "weak" capture can cause additional difficulties, even when using guard let to a weakly captured value inside the closure.

2. If you have a case of a cycle of strong references (entity A owns entity B, and entity B owns entity A), then in one of the cases you need to use “weak capturing” . It is necessary to take into account which of the two entities will be freed first, so if the view controller A represents the view controller B, then the view controller B may contain a “weak” link back to “A”.

3. If the possibility of a cycle of strong links is excluded, you can use "strong" capture ( "strong capturing" ). For example, performing an animation does not block self inside the closure containing the animation, so you can use "strong" binding.

4. If you are not sure, start with a “weak” binding and only change it if necessary.

Additionally - the official Swift guide:
Closures
Automatic reference counting

Source: https://habr.com/ru/post/444336/


All Articles