Control flow
A program wouldn't be very useful if it were a single fixed list of commands that always did the same thing. With a single code path, a calculator app would only be able to perform one operation. To make an app more powerful, there are a number of ways in which we can use the data to make decisions as to what to do next.
Conditionals
The most basic way to control the flow of a program is to specify certain code that should only be executed if a certain condition is met. In Swift, we do that with an if
statement. Let's look at an example:
if invitees.count > 20 { println("Too many people invited") }
Semantically, the preceding code reads, "If the number of invitees is greater than 20, print Too many people invited
. This example only executes one line of code if the condition is true, but you can put as much code as you like within the curly brackets ({}
).
Anything that can be evaluated as either true
or false
can be used in an if
statement. You can then chain multiple conditions together using an else if
and/or an else
statement:
if invitees.count > 20 { println("Too many people invited") } else if invitees.count <= 3 { println("Not really a party") } else { println("Just right") }
Each successive condition that is linked together will not even be evaluated unless all of the previous conditions fail. Similarly, once a condition passes, the remaining conditions in the chain are skipped.
As an exercise, I recommend you try adding an additional scenario to the previous code, where if there were exactly zero invitees, it would print One is the loneliest number
. You can test out your code by adjusting how many invitees you add to the invitees declaration. Remember that the order of the conditions is very important.
As useful as conditionals are, they can become very verbose if you have a lot of them chained together. To solve this type of problem, there is another control structure called a switch.
Switches
A switch is a more expressive way of writing a series of if
statements. A direct translation of the example from the conditionals section would look like this:
switch invitees.count { case let x where x > 20: println("Too many people invited") case let x where x <= 3: println("Not really a party") default: println("Just right") }
A switch consists of a value and a list of conditions for that value with the code to execute if the condition is true
. The value to be tested is written immediately after the switch
command and all of the conditions are contained within the curly brackets ({}
). Each condition is a called a case. Using that terminology, the semantics of the previous code is "Considering the number of invitees, in the case that it is greater than 20
, print Too many people invited
; otherwise, in the case that it is less than or equal to 3
, print Not really a party
; otherwise, by default, print Just right
.
The way that each of these cases work is that it creates a temporary variable x that is given the value that the switch is testing. It then performs a test on x. If the condition passes, it executes the code for that case and then exits the switch.
Just like in conditionals, each case is only considered if all the previous cases are not satisfied. Unlike conditionals, all the cases need to be exhaustive. This means that you need to have a case for every possible value that the variable passed in could be. For example, our invitees.count
value is an integer, so it could theoretically be any value from negative infinity to positive infinity.
The most common way to handle this is using a default
case, as designated by the default
keyword. Sometimes, you don't actually want to do anything in the default
case, or possibly, even in a specific
case. For that, you can use the break
keyword:
switch invitees.count { case let x where x > 20: println("Too many people invited") case let x where x <= 3: println("Not really a party") default: break }
Note that the default
case must always be the last case.
So far, switches are nice because they enforce the fact that they must be exhaustive. This is great for letting the compiler catch bugs for you. However, switches can also be much more concise. We can rewrite the preceding code like this:
switch invitees.count { case 0...3: println("Not really a party") case 4...20: println("Just right") default: println("Too many people invited") }
Here, we are describing each case as a range of possible values. The first case includes all the values between and including 0
and 3
. This is way more expressive than using a where
clause. It, however, did require a rethinking of the logic. There is no way to express an open-ended range that goes to infinity, so we could not specify a case for the number of invitees above 20. Instead, we have cases for the closed ranges that we know of and then capture all the required details for the above 20 cases in the default
case. Note that this version of the code does not properly handle the situation where the count might be negative, which the original version did. In this version, if the count is -1
, it will fall all the way through to the default
case and print out Too many people invited
. For this use case, this is ok because the count of an array can never be negative.
Switches don't only work with numbers, they are great to perform any type of test as well:
switch name { case "Marcos", "Amy": println("\(name) is an honored guest") case let x where x.hasPrefix("A"): println("\(name) will be invited first") fallthrough default: println("\(name) is someone else") }
This code shows some other interesting abilities for switches. The first case is actually made up of two separate conditions. Each case can have any number of conditions separated by commas (,
). This is useful when you have multiple cases that you want to use the exact code for.
The second case uses a custom test on the name to check whether it starts with the letter A
. This is also great for demonstrating the fundamental way in which switches are executed. Even though the string Amy
would also satisfy the second condition, this code would only print, "Amy is an honored guest"
because other cases are not evaluated once one case is satisfied. For now, don't worry if you don't completely understand how the hasPrefix
works.
Lastly, the second case uses the fallthrough
keyword. This tells the program to also execute the code in the following case. Importantly, this bypasses the condition of the next case; it doesn't matter if the value passes the condition, the code is still executed.
To make sure that you understand how a switch is executed, put the following code into a playground and try to predict what will be printed out with various names:
let testName = "Andrew" switch testName { case "Marcos", "Amy": println("\(name) is an honored guest") case let x where x.hasPrefix("A"): println("\(name) will be invited first") fallthrough case "Jamison": println("\(name) will help arrange food") default: println("\(name) is someone else") }
Some good names to try are "Andrew"
, "Amy"
, and "Jamison"
.
Now we have full control over what code we want executed and in what circumstances. However, a program often requires that we execute the same code more than once. For example, if we want to perform an operation on every element in an array, it would not be very maintainable to copy and paste a bunch of code. Instead, we can use control structures called loops.
Loops
There are multiple types of loops, but all of them offer a way to execute the same code repeatedly until a condition is no longer true
. The most basic type of loop is called a while
loop:
var index = 0 while index < invitees.count { println("\(invitees[index]) is invited") index++ }
A while
loop consists of a condition to be tested and the code to be run until that condition fails. In the preceding example, we looped through every element in the invitees
array. We used the variable index
to track which invitee we were currently on. To move to the next index, we use a new operator ++
, which adds 1 to the existing value. This is the same as writing index += 1
or index = index + 1
.
There are two important things to note about this loop. First, our index starts at 0
and not 1
and it goes until it is less than the number of invitees, not less than or equal. This is because, if you remember, array indexes start at 0
. If we started at 1
, we would miss the first element and if we included invitees.count
, the code would crash because it would try to access an element beyond the end of the array. Always remember that the last element of an array is always at the index: one less than the count.
The other thing to note is that if we forget to include index++
within the loop, we would have an infinite loop. The loop would continue to run forever because index
would never go beyond invitees.count
.
Note that this example while
loop is made up of three distinct types of operations. It has some initial set up (var index = 0
), it has a condition to test (index < invitees.count
), and it has code to run every iteration to set up the next value (index++
). This is such a common pattern that there is a loop called a for
loop that makes this more concise:
for var index = 0; index < invitees.count; index++ { println("\(invitees[index]) is invited") }
This type of loop is easier to read and it is also a little safer to write because the compiler will give an error if you forget any of those three steps. Each of the operations are separated by a semicolon (;
). The first operation is run once before the loop starts, the second operation is tested every time the loop is going to run, and the third is run at the end of every iteration.
However, wanting to loop through a collection or a series of values is so common that there is an even more concise and safe loop called a for
-in
loop:
for invitee in invitees { println("\(invitee) is invited") }
Now this is getting pretty cool. We no longer have to worry about indexes. There is no risk of accidentally starting at 1
or going past the end. Also, we get to give our own name to the specific element as we go through the array. Something to note here is that we did not declare the invitee
variable with let
or var
. This is special for a for
-in
loop because the constant used there is always newly declared each time through the loop.
For
-in
loops are great for looping through many types of containers. They can also be used to loop through a dictionary:
for (genre, show) in showsByGenre { println("\(show) is a great \(genre) series") }
In this case, we get access to both the key and value of the dictionary. This should look familiar because the value we are looping through with (genre, show
) is actually a tuple. It may be confusing for you to determine whether you are getting a single when looping over things like arrays or a tuple like when looping over dictionaries. At this point, it would be best for you to just remember these two common cases. The underlying reasons will become clear when we start talking about sequences in Chapter 6, Make Swift Work for You – Protocols and Generics.
These loops are great, but sometimes we do need access to the index we are currently on and at other times, we may want to loop through a set of numbers without an array. For this, we can use a range similar to a switch:
for index in 0 ..< invitees.count { println("\(index). \(invitees[index])") }
This code runs the loop using the variable index
from the value 0
, up to but not including invitees.count
. There are actually two types of ranges. The first one is called an open range because it does not include the last value. The other type of range, which we saw with switches, is called a closed range:
println("Counting to 10:") for number in 1 ... 10 { println(number) }
The closed range includes the last value, so this loop will print out every number starting from 1
and ending at 10
.
All loops have two special keywords available that let you modify their behavior called continue
and break
. The continue
keyword is used to skip the rest of the loop and move back to the condition to check whether the loop should be run again. For example, if we don't want to print invitees whose name begin with A
, we'll run the following code:
for invitee in invitees { if invitee.hasPrefix("A") { continue } println("\(invitee) is invited") }
If the condition invitee.hasPrefix("A")
passes, the continue
command is run and it skips the rest of the loop, moving onto the next invitee. Because of this, only the invitees whose names' do not start with A
are printed.
The break
keyword is used to immediately exit a loop:
for invitee in invitees { println("\(invitee) is invited") if invitee == "Tim" { println("Oh wait, Tim can't come") break } } println("Jumps here")
As soon as break
is encountered, the execution jumps to after the loop. In this case, it jumps to the final line.
Loops are great tools to deal with variable amounts of data, such as our list of invitees. When writing your code, you probably won't know how many people will be in that list. The use of a loop provides you with the flexibility to handle a list of any length.
As an exercise, I recommend you try to write a loop that will find the sum of all multiples of 3
under 10,000
. You should get 16668333
.
Loops are also a great way to reuse code without having to duplicate it, but they are just the first step toward quality code reuse. Next, we will talk about functions, which open up a whole new world of writing understandable and reusable code.