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

Original author: Paul Hudson
  • Transfer

Джозеф Райт, «Пленный» — иллюстрация «сильного» захвата

Список «захваченных» значений находится перед списком параметров замыкания и может «захватить» значения из области видимости тремя разными способами: используя ссылки «strong», «weak» или «unowned». Мы часто его используем, главным образом для того, чтобы избежать циклов сильных ссылок («strong reference cycles» aka «retain cycles»).
Начинающему разработчику бывает сложно принять решение, какой именно применить способ, так что вы можете потратить много времени, выбирая между «strong» и «weak» или между «weak» и «unowned», но, со временем, вы поймёте, что правильный выбор — только один.

Для начала создадим простой класс:

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 playSong () method of the Singer class :

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

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

let singFunction = sing()
singFunction()


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

Strong capturing


Unless you explicitly specify a capture method, Swift uses a “strong” capture. This means that the closure captures the external values ​​used and will never let them free.

Let's take a 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 a function, so under normal circumstances its place would be freed once the function has 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 end of the function.
This is a "strong" capture in action. If Swift allowed taylor to be freed , then calling the closure would be unsafe - its taylor.playSong () method is no longer valid.

"Weak" capture (weak capturing)


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

1. “Weakly” 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 paragraph, “weakly” captured values ​​in Swift are always optional .
We modify our example using a “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 how the values ​​should be captured. Here we say that taylor should be captured “weakly”, so we need to use taylor? .PlaySong () - now it 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 results in a message. The reason for this is that taylor exists only inside sing () , and the closure returned by this function does not holdtaylor is "strong" within himself.

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

"Ownerless" 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 abnormally in a similar fashion with the optionally deployed optional shown above — unowned taylor says: “I know for sure that taylor will exist for the duration of the closure, 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 extremely carefully.

Common problems


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

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


This is a common problem that you may encounter at the beginning of the study of closures, but, fortunately, Swift will help us in this case.

When using the capture list and the closure parameters together, the capture list comes in square brackets, then the closure parameters, then the in keyword, marking 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 closure parameters will result in a compilation error.

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


When an entity A has an entity B, and vice versa, you have a situation called a “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 defined the House class , which contains one property (closure), one method, and a de-initializer that will display a message when an instance of the class is destroyed.

Now create an Owner class 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 don’t need a catch block, but using a do block will destroy the instances right after}

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

As a result, 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 loop of strong links.

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 message “Creating a house and an owner” will appear, then “Done”. Deinitializers will not be called.

This happened as a result of 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 poor performance and even crash of the application.

To fix the situation, we need to create a new closure and use a “weak” capture 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, the situation of such an obvious cycle of strong links rarely arises, but this all the more speaks of the importance of using “weak” capture with competent development.

3. The inadvertent use of strong links, usually when capturing multiple values


Swift uses a strong grip by default, which can lead to unexpected 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 the 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 captured value.

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

[unowned taylor, unowned adele]

4. Copy closures and share captured values


The final case that developers stumble on is how faults are copied because the data they capture becomes available for all copies of the fault.
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 out every time the closure is called:

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

This will display the message “Lines logged: 1”.
Now we will create a copy of the closure that will share the captured values ​​along with the first closure. Thus, if 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 print out the messages “Lines logged: 1” ... “Lines logged: 4” because logger1 and logger2 point to the same captured numberOfLinesLogged variable .

When to use a "strong" capture, "weak" and "ownerless"


Now that 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 the closure, you can use “unowned capturing” . This is an infrequent situation where using “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 links (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 view controller A represents view controller B, then 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, executing 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 change it only if necessary.

Optional - The Official Swift Guide:
Closures
Automatic Link Counting

Also popular now: