What is defer used for
It's very simple. To summarize it in one sentence, the code in the defer block will be executed before the function returns, no matter which branch the function returns, there is a throw, or it will naturally go to the last line.
This keyword is the same as the try-catch-finally in Java. No matter which branch the try catch goes, it will be executed before the function return. And what's more powerful than Java's finally is that it can exist independently of try catch, so it can also become a small helper for organizing function processes. The processing you need to do anyway before returning the function can be put into this block to make the code look cleaner~
In fact, the origin of this article is because when refactoring Kingfisher, my understanding of defer was not accurate enough, which led to a bug. So I want to use this article to explore some edge cases of the keyword defer.
Typical usage
Everyone should be familiar with defer in Swift. The block declared by defer will be called after the current code is executed and exited. Because it provides a delayed call, it is generally used for resource release or destruction, which is particularly useful when a function has multiple return exits. For example, the following method of opening a file through FileHandle:
func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) let data = () if /* onlyRead */ { () return } let shouldWrite = /* Do you need to write a file */ guard shouldWrite else { () return } () (someData) () }
We need to call () in different places to close the file. The better way here is to use defer to handle it uniformly. This not only allows us to declare releases near the resource application place, but also reduces the possibility of forgetting to release resources when adding code in the future:
func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) defer { () } let data = () if /* onlyRead */ { return } let shouldWrite = /* Do you need to write a file */ guard shouldWrite else { return } () (someData) }
The scope of defer
When doing Kingfisher refactoring, I chose to use NSLock to ensure thread safety. Simply put, there will be some methods like this:
let lock = NSLock() let tasks: [ID: Task] = [:] func remove(_ id: ID) { () defer { () } tasks[id] = nil }
The operation for tasks may occur in different threads. Use lock() to acquire the lock and ensure that the current thread is exclusive. Then use unlock() to release the resource after the operation is completed. This is a typical way to use defer.
But later a situation occurred, that is, before calling the remove method, we acquired the lock in the caller of the same thread, for example:
func doSomethingThenRemove() { () defer { () } // Operation `tasks`// ... // Finally, remove `task`remove(123) }
This obviously creates a deadlock in remove: lock() in remove does unlock() operation while waiting for doSomethingThenRemove, and this unlock is blocked by remove and can never be achieved.
There are roughly three solutions:
- Use NSRecursiveLock: NSRecursiveLock can be obtained multiple times in the same thread without causing deadlock problems.
- unlock before calling remove.
- Pass in for remove by condition to avoid locking in it.
Both 1 and 2 both cause additional performance losses. Although such locking performance is minimal in general, using Scheme 3 does not seem to be very troublesome. So I happily changed the remove to this:
func remove(_ id: ID, acquireLock: Bool) { if acquireLock { () defer { () } } tasks[id] = nil }
OK, now calling remove(123, acquireLock: false) will no longer be deadlocked. But I soon discovered that the lock also failed when acquireLock is true. Read more carefully the description of Swift Programming Language about defer:
A 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 actually equivalent to:
func remove(_ id: ID, acquireLock: Bool) { if acquireLock { () () } tasks[id] = nil }
GG Smith...
I used to simply think that defer was called when the function exited, but I didn't notice that it was actually called when the scope exited, which caused this error. This should be paid special attention to when using defer in statements such as if, guard, for, try.
defer and closure
Another interesting fact is that although defer is followed by a closure, it is more like a syntactic sugar, which is different from the closure characteristics we are familiar with and does not hold the value inside. for example:
func foo() { var number = 1 defer { print("Statement 2: \(number)") } number = 100 print("Statement 1: \(number)") }
Will output:
Statement 1: 100
Statement 2: 100
If you want to rely on a variable value in defer, 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
The execution timing of defer
The execution time of defer is immediately after leaving scope, but before other statements. This feature brings some very "subtle" ways to use defer. For example, starting from 0:
class Foo { var num = 0 func foo() -> Int { defer { num += 1 } return num } // If there is no `defer`, we might have to write this// func foo() -> Int { // num += 1 // return num - 1 // } } let f = Foo() () // 0 () // 1 // 2
The output result foo() returns num before +1, and is the result after +1 in defer. Without using defer, it is actually difficult for us to achieve this effect of "operating after returning".
Although it is very special, it is strongly not recommended to perform such side effects 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 perspective, the purpose of defer is to clean up resources and avoid duplicate code that needs to be executed before returning, rather than to implement certain functions by tricks. Doing so will only make the code less readable.
Summarize
The above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.