3. Control Flow
Swift’s control structures hold no surprises in their shapes. Conditions are not
parenthesized, bodies are always braced, and nothing is truthy but a Bool. The
two things that repay attention are guard, which changes how you lay a
function out, and switch, which is the structure the rest of the language is
designed around.
3.1 if
if temp > 30 {
print("hot")
} else if temp > 15 {
print("mild")
} else {
print("cold")
}
The parentheses are optional and Swift programmers leave them off. The braces
are not optional, not even for one statement, which retires the class of bug
that made Apple’s own goto fail possible.
An if can also produce a value, which saves inventing a var and assigning to
it in each branch:
let size = if n < 10 { "small" } else { "large" }
Every branch must produce a value of the same type and every case must be
covered, so the else is required. switch works the same way as an
expression. Where a branch needs several statements the last one is the value.
For a single condition the conditional operator is shorter still, and reads the
way it does in C: let n = a > b ? a : b.
3.2 guard
guard states a condition that must hold for the rest of the function to make
sense, and its else block must leave: return, throw, break, continue,
or a call that never comes back.
// guard keeps the happy path flat and puts every exit at the top.
func check(_ text: String) -> String {
guard !text.isEmpty else {
return "no input"
}
guard let n = Int(text) else {
return "\"\(text)\" is not a number"
}
guard n >= 0, n < 150 else {
return "\(n) is not a plausible age"
}
return "age \(n)"
}
for input in ["", "abc", "-3", "41"] {
print(check(input))
}
no input
"abc" is not a number
-3 is not a plausible age
age 41
Written with if this is four levels of nesting and the real work is at the
bottom, squeezed against the right margin. Written with guard the failures are
at the top in the order you think of them and the real work is at the left, and
that is the shape most Swift functions take.
Two details make it work. A comma between conditions means “and”, and each one
is checked in order, so n >= 0, n < 150 is a single guard. And a name bound
by guard let stays in scope after the guard, for the rest of the enclosing
block, which is the reason to write guard let rather than if let.
Chapter 5 covers what let n = Int(text) is doing inside a condition.
3.3 Loops
while and repeat-while are C’s while and do-while under different
spelling. for-in walks anything that can be walked.
// The forms a loop takes.
let names = ["ada", "grace", "alan", "edsger"]
for (i, name) in names.enumerated() {
print(i, name)
}
for name in names where name.count > 4 {
print("long:", name)
}
for n in stride(from: 0, to: 20, by: 6) {
print(n, terminator: " ")
}
print()
var countdown = 3
repeat {
print("tick \(countdown)")
countdown -= 1
} while countdown > 0
0 ada
1 grace
2 alan
3 edsger
long: grace
long: edsger
0 6 12 18
tick 3
tick 2
tick 1
enumerated() pairs each element with its position, and the pair is taken apart
in the loop variable the way a tuple is. Reach for it rather than counting
indices by hand.
A where clause on a for skips the elements that fail it, which is continue
said earlier and more visibly.
stride(from:to:by:) is the loop C writes with three clauses. The to: form
excludes the end and through: includes it, and the step may be negative, which
is how you count down. A range cannot do that: 10...0 is a runtime error
rather than a backwards range, so stride(from: 10, through: 0, by: -1) or
(0...10).reversed() is how you go the other way.
3.4 break and continue
break leaves the nearest loop and continue starts its next turn. When the
loop you want to leave is not the nearest one, label it:
// Labeled break: stop both loops at the first match.
let grid = [
[3, 8, 1],
[9, 4, 6],
[7, 2, 5],
]
var found = "not found"
rows: for (r, row) in grid.enumerated() {
for (c, value) in row.enumerated() {
if value == 4 {
found = "row \(r), column \(c)"
break rows
}
}
}
print(found)
row 1, column 1
A label is a name and a colon in front of for, while, repeat or if, and
break label or continue label names it. This is the one place Swift offers
something that looks like a goto, it is confined to loops you can see on the
screen, and it beats the flag variable that the alternative requires.
Exercise 3-1. Rewrite search without a label, returning from a function
instead, and decide which you prefer.
Exercise 3-2. Print the numbers 1 to 100, replacing multiples of three with
fizz, multiples of five with buzz, and multiples of both with fizzbuzz.
Write it twice, once with if and once with a switch over a tuple of the two
remainders.
3.5 switch
A switch compares a value against patterns and runs the first arm that
matches. That much is C. Everything else about it is not.
// Classify a point by where it sits, using one switch over a tuple.
func describe(_ p: (x: Int, y: Int)) -> String {
switch p {
case (0, 0):
return "the origin"
case (_, 0):
return "on the x axis"
case (0, _):
return "on the y axis"
case let (x, y) where x == y:
return "on the diagonal at \(x)"
case let (x, y) where abs(x) <= 2 && abs(y) <= 2:
return "inside the box at \(x),\(y)"
default:
return "somewhere else"
}
}
for p in [(0, 0), (3, 0), (0, -4), (5, 5), (1, 2), (9, 4)] {
print(describe(p))
}
the origin
on the x axis
on the y axis
on the diagonal at 5
inside the box at 1,2
somewhere else
Four things are happening that C’s switch cannot do.
It matches a tuple, position by position, with _ standing for any value in
that position. It binds: case let (x, y) gives names to the parts of whatever
matched, and those names are in scope in the arm. It filters: a where clause
narrows an arm to the cases that also satisfy a condition. And it switches on
things that are not integers, since strings, ranges, tuples and the enums of
Chapter 8 are all matchable.
The two rules that change how you write code are these. Cases do not fall
through, so there is no break to forget and no bug when you do; the fallthrough
keyword exists for the rare time you want C’s behavior and it is rare. And a
switch must be exhaustive. Over an Int or a String that means you need a
default, which is why one is there above. Over a type with a fixed set of
values it means the compiler counts the cases for you:
non_exhaustive.swift:6:5: error: switch must be exhaustive
5 | func turn(_ d: Direction) -> String {
6 | switch d {
| |- error: switch must be exhaustive
| `- note: add missing case: '.west'
That message is the reason Chapter 8 argues for modeling your data as a fixed set of cases whenever the set really is fixed. Add a fifth direction a year from now and the compiler walks you to every place in the program that has to change, by file and line. No test suite gives you that and no amount of care replaces it.
Arms may list alternatives separated by commas, and may use ranges:
switch score {
case ..<0, 101...:
print("out of range")
case 0..<60:
print("fail")
case 60..<90:
print("pass")
default:
print("distinction")
}
An empty arm is not allowed. Where you want a case to do nothing you write
break, which says you meant it.
Exercise 3-3. Write a function that takes an HTTP status code and returns
its class as a string, using range patterns and no if.
Exercise 3-4. Rewrite describe so that the diagonal case reports whether
the point is on the rising or falling diagonal, without adding an if to the
body of an arm.
Exercise 3-5. Find out what fallthrough does to the value binding in the
arm it falls into, and why the compiler refuses some of those cases.