Swift. Closures
Here is a quick introduction, along with the full articles list
In-out Parameters
Function parameters are constant by default
In-out parameters are used to modify parameter values. So, after a function returns, a variable that is passed as &variable will have the modified value
var num1 = 3
var num2 = 5func swap(_ num1: inout Int, _ num2: inout Int) {
let temp = num1
num1 = num2
num2 = temp
}swap(&num1, &num2) // num1 = 5, num2 = 3
In-out parameters are passed as follows:
- when a function is called, the value of the argument is copied
- in the function body, the copy is modified
- when the function returns, the copy is assigned to the original argument
It is called copy-in copy-out behavior
Memory Safety Problem
In most cases, Swift manages memory automatically. Most of the time, there is no need to care about accessing memory at all. And even if there is conflicting access to memory from within a single thread, Swift guarantees that it will go along with a compile-time or runtime error
Conflicting access can occur when one part of the code changes a memory region, which is being updated by another part of the code at the same time. It can happen within a single thread. So, it is not about concurrency
Characteristics of memory access, which should be considered when telling about the conflicts:
- whether the memory access is read or write
- the duration of memory access is instantaneous or long-term
- the location in memory
Access is instantaneous if other code can’t run after the access starts but before it ends. Most memory accesses are instantaneous
Access is long-term if other code can run when the access starts but before it ends. It’s called overlap. Long-term access can overlap with other long-term or instantaneous accesses
The conflict happens if there are two or more accesses, and at least one of them is write, and all of them refer to the same memory region, and their durations overlap
Overlapping accesses appear primarily in code that uses in-out parameters in functions, and mutating methods of a structure
A function has long-term write access to all of its in-out parameters. Long-term access starts when all non-in-out parameters are evaluated and ends when the entire function ends
The first consequence is that it is impossible to have access to variables, which are passed as in-out parameters, from the outside scope during the function execution
var stepSize = 1func increment(_ number: inout Int) {
number += stepSize
}increment(&stepSize) // the runtime error// number and stepSize refer to the same variable
// in-out captures &stepSize for having write access,
// and the function captures stepSize for having read access
// A possible solutionvar stepSize = 1
var stepSizeCopy = stepSizefunc increment(_ number: inout Int) {
number += stepSize
}increment(&stepSizeCopy)
stepSize = stepSizeCopy
Another consequence of long-term write access to in-out parameters is that passing the same variable as multiple in-out argument of the same function produces a conflict
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}var firstPlayerScore = 42
var secondPlayerScore = 30balance(&firstPlayerScore, &secondPlayerScore)
// ok
balance(&firstPlayerScore, &firstPlayerScore)
// the compile-time error
A mutating method of a structure has long-term write access to self within the whole method call. Long-term access to self starts when the mutating method starts and ends when the entire method ends (self behaves like inout)
struct Player {
var health: Int
mutating func shareHealth(with player: inout Player) {
player.health = health
}
}var p1 = Player(health: 100)
var p2 = Player(health: 200)p2.shareHealth(with: &p1) // ok
p2.shareHealth(with: &p2) // the compile-time error
In terms of tuples, structures, and enums (value types), access to a single property means access to the whole structure
var playerInfo = (health: 10, energy: 20) // tuple
balance(&playerInfo.health, &playerInfo.energy)
// the compile-time errorvar player = Player(health: 200, energy: 150) // structure
balance(&player.health, &player.energy)
// the compile-time error
But in practice, most accesses to structure properties can overlap safely because they occur with local function variables instead of global ones. In this case, Swift can say for sure that the accesses are safe
func someFunc() {
var playerInfo = (health: 10, energy: 20)
balance(&playerInfo.health, &playerInfo.energy)
}
someFunc() // ok
Swift can confirm that overlapping access to structure is safe if the following requirements are satisfied:
- it’s accessing only to stored properties of the instance, but not computed or class ones
- the structure is a value of a local variable, but not a global one
- the structure is either not captured by any closure, or it’s captured only by a non-escaping one
If a nested escaping closure captures an in-out parameter, Swift throws a compile-time error
func someFunction(a: inout Int) -> () -> Int {
return { () in return a + 1 } // the compile-time error
}
// Possible solutions// capture list
func someFunction(a: inout Int) -> () -> Int {
return { [a] () in return a + 1 }
}// local variable
func someFunction(queue: DispatchQueue, x: inout Int) {
var localX = x
defer { x = localX } queue.async { someMutatingOperation(&localX) }
queue.sync {} // queue should be serial
}
By default, when a closure captures a variable from the surrounding context and changes the value inside, then the variable will also be changed in outside scope. No matter whether it is a value or reference type
struct Calculator {
var a: Int
var b: Int var sum: Int {
return a + b
}
}var calculator = Calculator(a: 3, b: 5)let closure = {
calculator = Calculator(a: 30, b: 50)
}// Calculator(3, 5)
closure()
// Calculator(30, 50)
The default behavior has a side effect if a variable gets changed in outside scope after the closure is defined but before it’s called. Then the closure will have the changed variable value
var calculator = Calculator(a: 3, b: 5)let closure = {
print(calculator.sum)
}calculator = Calculator(a: 30, b: 50)
closure()
// prints 80, but we wanted 8
To prevent the side effect, the closure must use a capture list. A capture list keeps immutable copies of variables (the copies are constant, an attempt to change them will produce a compile-time error)
var calculator = Calculator(a: 3, b: 5)let closure = { [calculator] in
print(calculator.sum)
}calculator = Calculator(a: 30, b: 50)
closure()
// prints 8
By default, if a class or structure method has an escaping closure, and the closure uses properties or methods of that class or structure, it is required to specify self explicitly (self.classMethod()). Otherwise, it will cause a compile-time error
Closures are reference types. They store strong references to the class instances that are captured from the surrounding context (no matter whether it is self or just a variable)
class Animal {
public let name: String init(name: String) {
self.name = name
} deinit {
print("\(name) is deinited")
}
}class AnimalHandler {
public var closure: (() -> Void)?
public func setPanda() {
let panda = Animal(name: "panda")
closure = { print(panda.name) }
}
}let animalHandler = AnimalHandler()
animalHandler.setPanda()
animalHandler.closure = nil
// panda is deinited only when the closure is deinited
class AnimalHandler {
public var closure: (() -> Void)?
public func setPanda() {
var panda: Animal? = Animal(name: "panda")
// capture list makes one more strong reference
closure = { [panda] in print(panda!.name) }
panda = nil // nothing happens
}
}let animalHandler = AnimalHandler()
animalHandler.setPanda()
animalHandler.closure = nil
// panda is deinited only when the closure is deinited
class AnimalHandler {
public var closure: (() -> Void)?
public func setPanda() {
var panda: Animal? = Animal(name: "panda")
closure = { [weak panda] in print(panda!.name) }
panda = nil // panda is deinited here
}
}let animalHandler = AnimalHandler()
animalHandler.setPanda()
For value types, a capture list makes an immutable copy of data, but for reference types, it makes one more strong reference by default if there are no weak or unowned words
A closure and its surrounding context are connected to each other. Let’s say that the closure has its own variables plus the surrounding context variables
If closure changes variables from the surrounding context, after calling the closure, the context will have updated values. And vice versa, if the context changes variables after defining the closure but before calling it, the closure will get updated values because both the context and the closure refer to the same context variables
A capture list in closures allows making own copies of data (value types) or own references to class instances (reference type) when the closure is defined. In this case, if the context changes or destructs the variables, the closure will still have its own copies
And if the closure is escaping (it is called after the function returns), it stores the context data even when the context doesn’t live anymore. Using escaping closures allows the context data to live till the closure is alive. But Swift also handles all memory management involved in disposing of variables when they are no longer needed. Thus, escaping closures store only those variables from the context that are really used
Escaping closures (@escaping) require using self explicitly for both reference and value type. If self is an instance of a structure or an enumeration, the escaping closure can’t capture a mutable reference to self. Structures and enumerations don’t allow sharing mutability
class SomeClass {
var x = 10
func doSomething() {
// ok because of class
someFuncWithEscapingClosure { self.x = 100 }
}
}// butstruct SomeStruct {
var x = 10
mutating func doSomething() {
// the compile-time error because of structure
someFuncWithEscapingClosure { self.x = 100 }
}
}
If an escaping closure is used as a completion handler or just a part of an async operation, the best practice there is capturing self as weak. So, if the function is being executed for a long time, weak self allows disposing of resources captured by the closure before the async logic completely ends
E.g., the user leaves a screen that triggered an async function of a service. And self (of the service) is captured by an escaping closure inside the function. Having weak self there allows to free resources of the service before the whole async logic is completed
// best practiceclass Service {
var requestState: String = "" public func getData(callback: @escaping (_ data: Any) -> Void) {
requestState = "request has been stared"
apiService.getDataFromServer() { [weak self] (data) in
self?.requestState = "request has been finished"
callback(data)
}
}
}
Advice: If a closure isn’t escaping, don’t think about memory leakages. Just use it and feel happy. But if it is, keep in mind memory leakage problems
Resources
- https://docs.swift.org/swift-book/LanguageGuide/Functions.html
- https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545
- https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html
- https://marcosantadev.com/capturing-values-swift-closures/
- https://docs.swift.org/swift-book/LanguageGuide/Closures.html