“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”
Recently, Swift 2.0 introduced two new control statements that aim to simplify and streamline the programs we write: guard
and defer
. While the first by its nature makes our code more linear, the other defers 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
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.
guard
is a new conditional statement that requires execution to exit the current block if the condition isn’t met. Any new optional bindings created in a guard
statement’s condition are available for the rest of the function or block, and the mandatory else
must exit the current scope, by using return
to leave a function, continue
or break
within a loop, or a @noreturn
function like fatalError()
:
forimageNameinimageNamesList{guardletimage=UIImage(named:imageName)else{continue}// do something with image}
Let’s take a before-and-after look at how guard
can improve our code and help prevent errors. As an example, we’ll build a new string-to-UInt8
initializer. UInt8
already declares a failable initializer that takes a String
, but if the conversion fails we don’t learn the reason—was the format invalid or was the value out of bounds for the numeric type? Our new initializer throws a ConversionError
that provides more information.
enumConversionError:ErrorType{caseInvalidFormat,OutOfBounds,Unknown}extensionUInt8{init(fromStringstring:String)throws{// check the string's formatiflet_=string.rangeOfString("^\\d+$",options:[.RegularExpressionSearch]){// make sure the value is in boundsifstring.compare("\(UInt8.max)",options:[.NumericSearch])!=NSComparisonResult.OrderedAscending{throwConversionError.OutOfBounds}// do the built-in conversionifletvalue=UInt8(string){self.init(value)}else{throwConversionError.Unknown}}throwConversionError.InvalidFormat}}
Note how far apart the format check and the invalid format throw
are in this example. Not ideal. Moreover, the actual initialization happens two levels deep, inside a nested if
statement. And if that isn’t enough, there’s a bug in the logic of this initializer that isn’t immediately apparent. Can you spot the flaw? What’s really going to bake your noodle later on is, would you still have noticed it if I hadn’t said anything?
Next, let’s take a look at how using guard
transforms this initializer:
extensionUInt8{init(fromStringstring:String)throws{// check the string's formatguardlet_=string.rangeOfString("^\\d+$",options:[.RegularExpressionSearch])else{throwConversionError.InvalidFormat}// make sure the value is in boundsguardstring.compare("\(UInt8.max)",options:[.NumericSearch])!=NSComparisonResult.OrderedDescendingelse{throwConversionError.OutOfBounds}// do the built-in conversionguardletvalue=UInt(string)else{throwConversionError.Unknown}self.init(value)}}
Much better. Each error case is handled as soon as it has been checked, so we can follow the flow of execution straight down the left-hand side.
Even more importantly, using guard
prevents the logic flaw in our first attempt: that final throw
is called every time because it isn’t enclosed in an else
statement. With guard
, the compiler forces us to break scope inside the else-block, guaranteeing the execution of that particular throw
only at the right times.
Also note that the middle guard
statement isn’t strictly necessary. Since it doesn’t unwrap an optional value, an if
statement would work perfectly well. Using guard
in this case simply provides an extra layer of safety—the compiler ensures that you leave the initializer if the test fails, leaving no way to accidentally comment out the throw
or introduce another error that would lose part of the initializer’s logic.
defer
Between guard
and the new throw
statement for error handling, Swift 2.0 certainly seems to be encouraging 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 new 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 this snippet of a function working with vImage
from the Accelerate framework, taken from the newly-updated article on image resizing:
funcresizeImage(url:NSURL)->UIImage?{// ...letdataSize:Int=...letdestData=UnsafeMutablePointer<UInt8>.alloc(dataSize)vardestBuffer=vImage_Buffer(data:destData,...)// scale the image from sourceBuffer to destBuffervarerror=vImageScale_ARGB8888(&sourceBuffer,&destBuffer,...)guarderror==kvImageNoErrorelse{destData.dealloc(dataSize)// 1returnnil}// create a CGImage from the destBufferguardletdestCGImage=vImageCreateCGImageFromBuffer(&destBuffer,&format,...)else{destData.dealloc(dataSize)// 2returnnil}destData.dealloc(dataSize)// 3// ...}
Here an UnsafeMutablePointer<UInt8>
is allocated for the destination data early on, but we need to remember to deallocate at both failure points and once we no longer need the pointer.
Error prone? Yes. Frustratingly repetitive? Check.
A defer
statement removes any chance of forgetting to clean up after ourselves while also simplifying our code. Even though the defer
block comes immediately after the call to alloc()
, its execution is delayed until the end of the current scope:
funcresizeImage(url:NSURL)->UIImage?{// ...letdataSize:Int=...letdestData=UnsafeMutablePointer<UInt8>.alloc(dataSize)defer{destData.dealloc(dataSize)}vardestBuffer=vImage_Buffer(data:destData,...)// scale the image from sourceBuffer to destBuffervarerror=vImageScale_ARGB8888(&sourceBuffer,&destBuffer,...)guarderror==kvImageNoErrorelse{returnnil}// create a CGImage from the destBufferguardletdestCGImage=vImageCreateCGImageFromBuffer(&destBuffer,&format,...)else{returnnil}// ...}
Thanks to defer
, destData
will be properly deallocated no matter which exit point is used to return from the function.
Safe and clean. Swift at its best.
defer
blocks are executed in the reverse order of their appearance. 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.
(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:
postfixfunc++(inoutx:Int)->Int{letcurrent=xx+=1returncurrent}
In this case, defer
offers a clever alternative. Why create a temporary variable when we can just defer the increment?
postfixfunc++(inoutx:Int)->Int{defer{x+=1}returnx}
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 guard against confusion and obscurity.