UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

The defer keyword in Swift: try/finally done right

Paul Hudson    September 23rd 2019    @twostraws

Swift's defer keyword lets us set up some work to be performed when the current scope exits. For example, you might want to make sure that some temporary resources are cleaned up once a method exits, and defer will make sure that happens no matter how that exit happens.

How it used to be: messy returns

Here's some dummy Swift code that opens a file, writes some data, then closes the file:

func writeLog() {
    let file = openFile()

    let hardwareStatus = fetchHardwareStatus()
    guard hardwareStatus != "disaster" else { return }
    file.write(hardwareStatus)

    let softwareStatus = fetchSoftwareStatus()
    guard softwareStatus != "disaster" else { return }
    file.write(softwareStatus)

    let networkStatus = fetchNetworkStatus()
    guard neworkStatus != "disaster" else { return }
    file.write(networkStatus)

    closeFile(file)
}

I'm not going to try to fill in any code for the methods in there, they are hopefully self-explanatory. As you can see, a file is opened, then various types of data are written out, before finally the file is closed.

But what happens if any one of those status checks returns "disaster"? Answer: our guard condition will trap the error and exit the method – leaving the file open.

There were two solutions to this, neither of which were nice. The first was to copy and paste the call to closeFile() so that it is called before any of those returns. The second was to create a pyramid of doom, with several stacked conditional statements to handle writing.

Swift 2 solves this problem, but does so in a simple and clear way: the defer keyword.

Deferring work in Swift 2: defer

Swift 2 introduces the defer keyword, which effectively means "here's some what I want you to do later, no matter what." That work can be whatever you want: a single method call closing a file, or 50 lines of code doing some other important clean up work. The important thing is that Swift ensures that it will be run before the current scope is ended.

So, we could rewrite the above code like this:

func writeLog() {
    let file = openFile()
    defer { closeFile(file) }

    let hardwareStatus = fetchHardwareStatus()
    guard hardwareStatus != "disaster" else { return }
    file.write(hardwareStatus)

    let softwareStatus = fetchSoftwareStatus()
    guard softwareStatus != "disaster" else { return }
    file.write(softwareStatus)

    let networkStatus = fetchNetworkStatus()
    guard neworkStatus != "disaster" else { return }
    file.write(networkStatus)
}

With that defer call in place, closeFile() will be called no matter which of the guards are triggered, or even if none of them trigger and the method completes normally.

That's what I mean when I say that deferred work will always take place: if you return from a method at the end or part way through, or if you exit a method by throwing an error, your deferred work will take place.

This makes it perfect for ensuring code is cleaned up under all conditions, and is similar to try/finally in other languages.

Defer scope in Swift 2

Using defer inside a method means that its work will be executed as the method is exiting. For example:

override func viewDidLoad() {
    super.viewDidLoad()

    print("Step 1")
    myFunc()
    print("Step 5")
}

func myFunc() {
    print("Step 2")
    defer { print("Step 3") }
    print("Step 4")
}

That will print "Step 1", "Step 2", "Step 4", "Step 3", "Step 5" – steps 3 and 4 are switched because 3 is deferred until the myFunc() method ends, i.e. when it goes out of scope programmatically.

This scope is effectively anything in braces, { and }, but realistically there are two main ways you may want to use it: inside a do block and inside a loop.

do blocks are usually used with catch to handle errors smoothly, but you don't need that – you can use do to create a temporary scope anywhere inside your method. Any variables created inside the do block exist only there, and any work you defer inside the block will execute as soon as the closing brace of the block is reached.

For example:

print("Step 1")

do {
    defer { print("Step 2") }
    print("Step 3")
    print("Step 4")
}

print("Step 5")

When that runs, you'll see 1, 3, 4, 2, 5 because 2 is deferred until the end of the do block.

As I said, loops are also natural places you might want to use defer, and it will execute at the end of each loop iteration. For example:

for i in 1...10 {
    print ("In \(i)")
    defer { print ("Deferred \(i)") }
    print ("Out \(i)")
}

You can defer multiple things

One of the most powerful features of defer is that you can stack up multiple deferred pieces of work, and Swift will ensure they all get executed. What's more, it executes them in reverse order, meaning that the most recently deferred thing gets run first – effectively unwinding a stack.

Not only does this mean you can defer work without having to worry about what if anything was already deferred, but also that Swift safely unwinds its defer stack based on the order you chose.

There is one catch, albeit a minor one: your defer calls shouldn't try to exit the current scope using something like a return call or throwing an error. Other than that, you're good to go!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.