Several simple usage scenarios of defer in DETAIL

  • 2020-06-07 05:22:15
  • OfStack

preface

Recently I was going to scan the swift document again and found the keyword defer. defer is a very important swift language feature. I am too stupid to have used this before.

What does defer do

Very simply, in one sentence, the code in defer block will be executed before the function return, regardless of which branch it comes from, throw, or the last line.

This keyword, like try-ES22en-ES23en 1 in Java, is executed before the function return regardless of which branch try catch takes. And one thing that makes it even more powerful than Java's finally is that it can exist independently of try catch, so it can also be a small helper in organizing the flow of functions. The processing that would have been done before return anyway could be put into this block to make the code look cleaner

Here is an example from the swift documentation:


var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
 fridgeIsOpen = true
 defer {
  fridgeIsOpen = false
 }
 let result = fridgeContent.contains(food)
 return result
}
fridgeContains("banana")
print(fridgeIsOpen)

The order of execution in this example is fridgeIsOpen = true, then the normal flow of the function body, and finally fridgeIsOpen = false before return.

A few simple usage scenarios

try catch structure

The most typical scenario, I think, is the main reason for the emergence of the keyword defer:


func foo() {
 defer {
 print("finally")
 }
 do {
 throw NSError()
 print("impossible")
 } catch {
 print("handle error")
 }
}

Whether do block throw error, catch arrives, or throw goes out, defer is guaranteed to execute before the entire function return. In this case, print says "handle error" and then print says "finally".

do block defer:


do {
 defer {
 print("finally")
 }
 throw NSError()
 print("impossible")
} catch {
 print("handle error")
}

Then the order of execution would be catch block, that is, print "finally" and print "handle error".

Clean up and recycle resources

Similar to the example given in the swift document, a good use case for defer 1 is for cleaning up. File manipulation is a good example:

Close the file


func foo() {
 let fileDescriptor = open(url.path, O_EVTONLY)
 defer {
 close(fileDescriptor)
 }
 // use fileDescriptor...
}

That way, you don't have to worry about which branch you forget to write, or the middle throw error, causing fileDescriptor to fail to close properly. There are also some similar scenes:

dealloc manually allocated space


func foo() {
 let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
 defer {
 valuePointer.deallocate(capacity: 1)
 }
 // use pointer...
}

Add/unlock: The following is one way of writing synchronized block similar to ES109en-ES110en. You can use any NSObject as lock


func foo() {
 objc_sync_enter(lock)
 defer { 
 objc_sync_exit(lock)
 }
 // do something...
}

For paired methods like this, you can use defer to start them at 1.

Adjustable completion block

This is a scenario that makes me feel like "if I had known defer", that is, sometimes there are too many branches for one function, maybe a small branch return forgot to tune completion before, and the result is a hidden bug that is not easy to find. With defer, you don't have to worry about this:


func foo(completion: () -> Void) {
 defer {
 self.isLoading = false
 completion()
 }
 guard error == nil else { return } 
 // handle success
}

Sometimes completion passes different parameters depending on the situation, and defer does not work well. But if completion block is saved, we can still use it to ensure that it is released after execution:


func foo() {
 defer {
 self.completion = nil
 }
 if (succeed) {
 self.completion(.success(result))
 } else {
 self.completion(.error(error))
 }
}

Adjustable super method

Sometimes override has 1 method and the main purpose is to do 1 preparatory work before super method, such as prepare(forCollectionViewUpdates:) of UICollectionViewLayout, then we can put the part that calls super in defer:


func override foo() {
 defer {
 super.foo()
 }
 // some preparation before super.foo()...
}

1 some of the details

Any scope can have defer

Although most usage scenarios are in functions, it is theoretically possible to write defer between any 1 {}. Take a normal loop:


var sumOfOdd = 0
for i in 0...10 {
 defer {
 print("Look! It's \(i)")
 }
 if i % 2 == 0 {
 continue
 }
 sumOfOdd += i
}

Neither continue nor break will interfere with the implementation of defer. Even a plain defer can be written defer:


func foo() {
 defer {
 print("finally")
 }
 do {
 throw NSError()
 print("impossible")
 } catch {
 print("handle error")
 }
}
0

It just doesn't make any sense...

Must execute until defer to trigger
Suppose you have a question like this: can defer in 1 scope guarantee that 1 will execute? The answer is... Take the following example:


func foo() {
 defer {
 print("finally")
 }
 do {
 throw NSError()
 print("impossible")
 } catch {
 print("handle error")
 }
}
1

Not defer, not print anything. The moral of the story is that it takes at least line 1 of defer to ensure that it will trigger later. In the same way, return in advance is no good:


func foo() {
 defer {
 print("finally")
 }
 do {
 throw NSError()
 print("impossible")
 } catch {
 print("handle error")
 }
}
2

Multiple defer

One scope can have more than one defer, and the order is executed backwards like stack 1: every defer encountered is like pushing one stack, and at the end of scope, the last stack executes first. The following code will be print in order 1, 2, 3, 4, 5, 6.


func foo() {
 print("1")
 defer {
 print("6")
 }
 print("2")
 defer {
 print("5")
 }
 print("3")
 defer {
 print("4")
 }
}

But I strongly recommend against it. I recommend not having more than one defer per scope, as it doesn't do much good except confuse the code readers.

conclusion


Related articles: