Previously, we learned to unwrap an optional with
if let
. That let us use the non-optional value inside a block of code. In Swift 1, that's all we had. Very soon, advanced developers complained about some esoteric phrases like "pyramid of doom", and "preferring early exit". Let's see an example by writing a simple function that takes 4 optional arguments and works with their values
func use(value0:String?, value1:String?, value2:String?, value3:String?, )->String? {
}
Now we'll add code which checks whether each of these values actually exists, has unique error code for each one, and finally executes some code if all the values are non-
nil
.
func use(value0:String?, value1:String?, value2:String?, value3:String?) {
if let value0:String = value0 {
if let value1:String = value1 {
if let value2:String = value2 {
if let value3:String = value3 {
//code involving all 4 values
print( value0 + value1 + value 2 + value3)
} else {
// error condition specific to lacking value3
}
} else {
//error condition specific to lacking value2
return
}
} else {
//error condition specific to lacking value1
}
} else {
//error condition specific to lacking value0
return nil
}
}
Now, if you're looking at this block of code and thinking, "I have no idea what this does or why it's written this way." Great! That's the idea. This is an anti-pattern.
The "pyramid of doom" is the white space along the left side of the function. It’s "base" is the left edge, and it’s "peak" is around
//code involving all 4 values
. See that? Once you do, you can’t un-see it. Notice, also, that we have specific code associated with failures of the specific values. The error code for the first value is down at the bottom of the function, and after the code that would run if we passed the test! This takes the developer’s mind out of context, and makes it more difficult to reason about the errors. Yes, you could figure out what happens in this function, but why waste the energy? We prefer something called "early exit". In other words, when a precondition for execution of the function fails, return before doing the work.
Swift supports "early exit" with the keyword
guard
. Like
if let
,
guard let
evaluates an
Optional
and declares a new value which is non-optional. Like
if let
, we declare an
else
block, and we must declare an
else
block, and what’s more, we can do whatever we want inside the
else
block, as long as we exit scope. Unlike an
if let
,
guard
extends the new value’s scope to the remainder of the existing scope, so my pyramid of doom turns into this:
func use(value0:String?, value1:String?, value2:String?, value3:String?) {
guard let value0:String = value0 else {
//error condition specific to lacking value 0
return
}
guard let value1:String = value1 else {
//error condition specific to lacking value 1
return
}
guard let value2:String = value2 else {
//error condition specific to lacking value 2
return
}
guard let value3:String = value3 else {
// error condition specific to lacking value3
return
}
//code involving all 4 values
print(value0 + value1 + value 2 + value3)
}
No more pyramid scheme. The code for handling each precondition failure is visually associated with that check that determines the failure. Our code very clearly talks about each condition that needs to happen before regular execution can take place, and then provides non-optional values for the regular code.
Inside the
else
block, "exit scope" means
break
or
continue
if we're inside a
repeat
,
while
or
for
loop. For a function, it means
return
.
There's another optimization we can make, both
if
and
guard
support the idea of multiple conditions, separated by commas.
guard let value0:String = value0
,let value1:String = value1
,let value2:String = value2
,let value3:String = value3
else {
// error condition for any missing value
return
}
And our
else
block now responds in a generic way to any errors, instead of specifically to each one. Not so esoteric now, is it? "Prefer early exit." It's actually the difference between legible code and completely-insanely unfathomable code. Which one do you want to read?
I know, you're asking why I put the commas at the beginning of the lines. It has to do with years of merge conflicts from adding items to lists. Fortunately, Swift is smart enough to know what we're doing.
Consider for a moment that our conditions are a bit stronger than that we must have a non-
nil
value for each string. What if
value1
and
value3
must not be "empty", i.e. a zero-length String?
guard let value0:String = value0
,let value1:String = value1
,!value1.isEmpty
,let value2:String = value2
,let value3:String = value3
,!value3.isEmpty
else {
// error condition for any missing value
return
}
Swift lets us freely mix in Bool and
let
expressions in any order in an
if
or
guard
. They are implicitly combined with "and". And they support shortcuts. In other words, if
value1
was empty, Swift wouldn't get around to trying to unwrap
value2
.
Let's continue with some other ways to control flow.
??
Suppose that we have an
Optional
and our code needs a definite value, perhaps by providing a default value to replace
nil
.
let userPreferredFileExtension:String?
Here we have a
String?
to represent the user's choice of a file extension, which will be
nil
if they do not provide one. But my file writing code demands having a file extension, so I need to use a default if the user didn't specify one. Swift provides a nifty
??
operator:
let fileExtension:String = userPreferredFileExtension ?? ".txt"
The
??
operator checks if the preceding value is
nil
and returns it if it is not. If it is nil, it continues with the next expression. In this case, we've used the
??
to guarantee that we will have a
String
so any additional code can treat
fileExtension
as non-optional. I think you'll find using the power of optionals and
??
simplifies tons of code that uses caches and defaults.
while
Swift also has a
while
statement, which is the same as C
while
statement, with the same changes we had for an
if
.
var a:Int = 0
while a < 3 {
print("a == \(a)")
a += 1
}
//a == 0
//a == 1
//a == 2
Note that we've used the
+=
operator, because Swift doesn't have a
++
or
--
operator. Yeah, some folks complained when they removed
++
, but it didn't take long to do a project wide search and replace with
+= 1
, so they moved on.
There is also a
repeat
-
while
, which is the Swift version of C's
do
-
while
. The
do
keyword in Swift is used for error handling, and we'll cover that in a future week. I would give you an example of
repeat
-
while
, but I can't confirm that I've ever actually written one; so I'm going to skip it.
Summary
Today we learned how to use
guard
-
let
-
else
to provide early exit where conditions are not ripe for our regular code to continue.
We also learned about providing default values in place of Optionals.
Last, we learned about while loops.
Supporting early-exit with
Optional
s? Now that's Swift!
Comments for
guard,whileand??