In fact, the origin of this article was due to a bug caused by my inaccurate understanding of defer when Kingfisher was refactoring. So I want to use this article to explore some edge cases for the keyword defer. The original | address

Typical usage

Defer from Swift should be familiar. The block declared by defer is called when the current code exits. Because it provides a way to delay calls, it is often used for resource release or destruction, which is especially useful when a function has multiple return exits. For example, using FileHandle to open a file:

func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) let data = fileHandle.readDataToEndOfFile() if /* onlyRead */ { fileHandle.closeFile() return } let ShouldWrite = / * whether need to write file * / guard shouldWrite else {fileHandle. CloseFile () return} fileHandle. SeekToEndOfFile () fileHandle.write(someData) fileHandle.closeFile() }Copy the code

We need to call filehandle.closefile () in different places to close the file, and it is better to defer in this case. This not only allows us to declare the resource to be released close to where it was requested, but also reduces the possibility of forgetting to release the resource when adding code in the future:

func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) defer { fileHandle.closeFile() } let data = fileHandle.readDataToEndOfFile() if /* onlyRead */ { return } Let shouldWrite = / * whether need to write file * / guard shouldWrite else {return} fileHandle. SeekToEndOfFile () fileHandle.write(someData) }Copy the code

deferThe scope of the

When doing Kingfisher refactoring, I chose to use NSLock to ensure thread safety. In short, there are some methods like this:

let lock = NSLock()
let tasks: [ID: Task] = [:]

func remove(_ id: ID) {
    lock.lock()
    defer { lock.unlock() }
    tasks[id] = nil
}
Copy the code

For Tasks, which can happen in different threads, lock() is used to acquire the lock and keep the current thread exclusive, and unlock() is used to release the resource when the operation is complete. This is a typical use of defer.

The lock was acquired from the caller of the same thread before the remove method was called.

Func doSomethingThenRemove() {lock.lock() defer {lock.unlock()} // Finally remove 'task' remove(123)}Copy the code

This apparently creates a deadlock in the remove: The lock() in remove is waiting for doSomethingThenRemove to do unlock(), which is blocked by remove and can never be reached.

There are three possible solutions:

  1. Switch to aNSRecursiveLock:NSRecursiveLockIt can be fetched multiple times on the same thread without causing deadlock problems.
  2. In the callremoveBefore youunlock.
  3. forremoveThe pass is conditional to avoid locking in it.

Both 1 and 2 cause an additional performance penalty, and while such locking performance is minimal in general, using scenario 3 does not seem too cumbersome. So I was very happyremoveIt was changed to this:

func remove(_ id: ID, acquireLock: Bool) {
    if acquireLock {
        lock.lock()
        defer { lock.unlock() }
    }
    tasks[id] = nil
}
Copy the code

AcquireLock (123, acquireLock: false) is no longer deadlocked. However, I soon discover that the lock also fails when acquireLock is set to true. Take a closer look at Swift Programming Language’s description of defer:

defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.

So, the above code is equivalent to:

func remove(_ id: ID, acquireLock: Bool) {
    if acquireLock {
        lock.lock()
        lock.unlock()
    }
    tasks[id] = nil
}
Copy the code

GG Smithers…

I had simply assumed that defer was called when the function exited, not noticing the fact that it was called when the current scope exited, which caused this error. You should pay special attention to this when using defer in if, guard, for, and try statements.

deferAnd closures

Another interesting fact is that although defer is followed by a closure, it is more like a syntactic sugar and, unlike the closure features we are familiar with, does not hold the values inside. Such as:

func foo() {
    var number = 1
    defer { print("Statement 2: (number)") }
    number = 100
    print("Statement 1: (number)")
}
Copy the code

Will print:

Statement 1: 100
Statement 2: 100
Copy the code

In defer, if you rely on a variable value, you need to copy it yourself:

func foo() {
    var number = 1
    var closureNumber = number
    defer { print("Statement 2: (closureNumber)") }
    number = 100
    print("Statement 1: (number)")
}

// Statement 1: 100
// Statement 2: 1
Copy the code

deferExecution timing of

Defer executes immediately after it leaves scope, but before the other statements. This feature brings some subtle ways to use defer. For example, increment from 0:

Class Foo {var num = 0 func Foo () -> Int {defer {num += 1} return num} Int { // num += 1 // return num - 1 // } } let f = Foo() f.foo() // 0 f.foo() // 1 f.num // 2Copy the code

The output foo() returns num before +1, and f.num is the result after +1 in defer. Without defer, it would have been difficult to achieve this “act on return” effect.

While this is unusual, it is strongly discouraged to implement this kind of Side effect in defer.

This means that a defer statement can be used, for example, to perform manual resource management such as closing file descriptors, and to perform actions that need to happen even if an error is thrown.

From a language design point of view, defer is intended to clean up resources and avoid repetitive code that needs to be executed before the return, rather than to be used to cheat on some functionality. Doing so only makes the code less readable.

IOS development technical materials