guard & defer
“We should do (as wise programmers aware of our limitations) our utmost best to … make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.”
—Edsger W. Dijkstra, “Go To Considered Harmful”
It’s a shame that his essay is most remembered for popularizing the “____ Consider Harmful” meme among programmers and their ill-considered online diatribes. Because (as usual) Dijkstra was making an excellent point: the structure of code should reflect its behavior.
Swift 2.0 introduced two new control statements
that aimed to simplify and streamline the programs we write:
guard
and defer
.
While the former by its nature makes our code more linear,
the latter does the opposite by delaying execution of its contents.
How should we approach these new control statements?
How can guard
and defer
help us clarify
the correspondence between the program and the process?
Let’s defer defer
and first take on guard
.
guard
guard
is a conditional statement
requires an expression to evaluate to true
for execution to continue.
If the expression is false
,
the mandatory else
clause is executed instead.
func say Hello(number Of Times: Int) {
guard number Of Times > 0 else {
return
}
for _ in 1...number Of Times {
print("Hello!")
}
}
The else
clause in a guard
statement
must exit the current scope by using
return
to leave a function,
continue
or break
to get out of a loop,
or a function that returns Never
like fatal
.
guard
statements are most useful when combined with optional bindings.
Any new optional bindings created in a guard
statement’s condition
are available for the rest of the function or block.
Compare how optional binding works with a guard-let
statement
to an if-let
statement:
var name: String?
if let name = name {
// name is nonoptional inside (name is String)
}
// name is optional outside (name is String?)
guard let name = name else {
return
}
// name is nonoptional from now on (name is String)
If the multiple optional bindings syntax introduced in
Swift 1.2
heralded a renovation of the
pyramid of doom,
guard
statements tear it down altogether.
for image Name in image Names List {
guard let image = UIImage(named: image Name)
else { continue }
// do something with image
}
Guarding Against Excessive Indentation and Errors
Let’s take a before-and-after look at how guard
can
improve our code and help prevent errors.
As an example,
we’ll implement a read
function:
enum Story Error: Error {
case missing
case illegible
case too Scary
}
func read Bedtime Story() throws {
if let url = Bundle.main.url(for Resource: "book",
with Extension: "txt")
{
if let data = try? Data(contents Of: url),
let story = String(data: data, encoding: .utf8)
{
if story.contains("👹") {
throw Story Error.too Scary
} else {
print("Once upon a time... \(story)")
}
} else {
throw Story Error.illegible
}
} else {
throw Story Error.missing
}
}
To read a bedtime story, we need to be able to find the book, the storybook must be decipherable, and the story can’t be too scary (no monsters at the end of this book, please and thank you!).
But note how far apart the throw
statements are from the checks themselves.
To find out what happens when you can’t find book.txt
,
you need to read all the way to the bottom of the method.
Like a good book, code should tell a story: with an easy-to-follow plot, and clear a beginning, middle, and end. (Just try not to write too much code in the “post-modern” genre).
Strategic use of guard
statements
allow us to organize our code to read more linearly.
func read Bedtime Story() throws {
guard let url = Bundle.main.url(for Resource: "book",
with Extension: "txt")
else {
throw Story Error.missing
}
guard let data = try? Data(contents Of: url),
let story = String(data: data, encoding: .utf8)
else {
throw Story Error.illegible
}
if story.contains("👹") {
throw Story Error.too Scary
}
print("Once upon a time... \(story)")
}
Much better! Each error case is handled as soon as it’s checked, so we can follow the flow of execution straight down the left-hand side.
Don’t Not Guard Against Double Negatives
One habit to guard against as you embrace this new control flow mechanism is overuse — particularly when the evaluated condition is already negated.
For example, if you want to return early if a string is empty, don’t write:
// Huh?
guard !string.is Empty else {
return
}
Keep it simple. Go with the (control) flow. Avoid the double negative.
// Aha!
if string.is Empty {
return
}
defer
Between guard
and the new throw
statement for error handling,
Swift encourages a style of early return
(an NSHipster favorite) rather than nested if
statements.
Returning early poses a distinct challenge, however,
when resources that have been initialized
(and may still be in use)
must be cleaned up before returning.
The defer
keyword provides a safe and easy way to handle this challenge
by declaring a block that will be executed
only when execution leaves the current scope.
Consider the following function that wraps a system call to gethostname(2)
to return the current hostname
of the system:
import Darwin
func current Host Name() -> String {
let capacity = Int(NI_MAXHOST)
let buffer = Unsafe Mutable Pointer<Int8>.allocate(capacity: capacity)
guard gethostname(buffer, capacity) == 0 else {
buffer.deallocate()
return "localhost"
}
let hostname = String(c String: buffer)
buffer.deallocate()
return hostname
}
Here, we allocate an Unsafe
early on
but we need to remember to deallocate it
both in the failure condition and once we’re finished with the buffer.
Error prone? Yes. Frustratingly repetitive? Check.
By using a defer
statement,
we can remove the potential for programmer error and simplify our code:
func current Host Name() -> String {
let capacity = Int(NI_MAXHOST)
let buffer = Unsafe Mutable Pointer<Int8>.allocate(capacity: capacity)
defer { buffer.deallocate() }
guard gethostname(buffer, capacity) == 0 else {
return "localhost"
}
return String(c String: buffer)
}
Even though defer
comes immediately after the call to allocate(capacity)
,
its execution is delayed until the end of the current scope.
Thanks to defer
, buffer
will be properly deallocated
regardless of where the function returns.
Consider using defer
whenever an API requires calls to be balanced,
such as allocate(capacity:)
/ deallocate()
,
wait()
/ signal()
, or
open()
/ close()
.
This way, you not only eliminate a potential source of programmer error,
but make Dijkstra proud.
“Goed gedaan!” he’d say, in his native Dutch.
Deferring Frequently
If you use multiple defer
statements in the same scope,
they’re executed in reverse order of appearance —
like a stack.
This reverse order is a vital detail,
ensuring everything that was in scope when a deferred block was created
will still be in scope when the block is executed.
For example, running the following code prints the output below:
func procrastinate() {
defer { print("wash the dishes") }
defer { print("take out the recycling") }
defer { print("clean the refrigerator") }
print("play videogames")
}
play videogames
clean the refrigerator
take out the recycling
wash the dishes
Deferring Judgement
If a variable is referenced in the body of a defer
statement,
its final value is evaluated.
That is to say:
defer
blocks don’t capture the current value of a variable.
If you run this next code sample, you’ll get the output that follows:
func flip Flop() {
var position = "It's pronounced /ɡɪf/"
defer { print(position) }
position = "It's pronounced /dʒɪf/"
defer { print(position) }
}
It's pronounced /dʒɪf/
It's pronounced /dʒɪf/
Deferring Demurely
Another thing to keep in mind
is that defer
blocks can’t break out of their scope.
So if you try to call a method that can throw,
the error can’t be passed to the surrounding context.
func burn After Reading(file url: URL) throws {
defer { try File Manager.default.remove Item(at: url) }
// 🛑 Errors not handled
let string = try String(contents Of: url)
}
Instead,
you can either ignore the error by using try?
or simply move the statement out of the defer
block
and at the end of the function to execute conventionally.
(Any Other) Defer Considered Harmful
As handy as the defer
statement is,
be aware of how its capabilities can lead to confusing,
untraceable code.
It may be tempting to use defer
in cases
where a function needs to return a value that should also be modified,
as in this typical implementation of the postfix ++
operator:
postfix func ++(inout x: Int) -> Int {
let current = x
x += 1
return current
}
In this case, defer
offers a clever alternative.
Why create a temporary variable when we can just defer the increment?
postfix func ++(inout x: Int) -> Int {
defer { x += 1 }
return x
}
Clever indeed, yet this inversion of the function’s flow harms readability.
Using defer
to explicitly alter a program’s flow,
rather than to clean up allocated resources,
will lead to a twisted and tangled execution process.
“As wise programmers aware of our limitations,” we must weigh the benefits of each language feature against its costs.
A new statement like guard
leads to a more linear, more readable program;
apply it as widely as possible.
Likewise, defer
solves a significant challenge
but forces us to keep track of its declaration as it scrolls out of sight;
reserve it for its minimum intended purpose to prevent confusion and obscurity.