Learning Swift(Second Edition)
上QQ阅读APP看书,第一时间看更新

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. There are a number of things we can do to make an app more powerful and collect 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 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 {
   print("Too many people invited")
}

Semantically, the preceding code reads; if the number of invitees is greater then 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 inside 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:

if invitees.count > 20 {
    print("Too many people invited")
}
else if invitees.count <= 3 {
    print("Not really a party")
}
else {
    print("Just right")
}

Each condition is checked from top to bottom until a condition is satisfied. At that point, the code block is executed and the remaining conditions are skipped, including the final else block.

As an exercise, I recommend adding an additional scenario to the preceding code in which, 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:
        print("Too many people invited")
    case let x where x <= 3:
        print("Not really a party")
    default:
        print("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 in curly brackets ({}). Each condition is called a case. Using that terminology, the semantics of the preceding 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 three, print "Too many people invited", otherwise, by default print "Just right".

This works by creating a temporary constant 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 of the previous cases are not satisfied. Unlike conditionals, all the cases need to be exhaustive. That means that you need to have a case for every possible value that the variable being passed in could be. For example, invitees.count is an integer, so it could theoretically be any value from negative infinity to positive infinity.

The most common way to handle that is by 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, as shown here:

switch invitees.count {
    case let x where x > 20:
        print("Too many people invited")
    case let x where x <= 3:
        print("Not really a party")
    default:
        break
}

Note that the default case must always be the last one.

We have seen so far that switches are nice because they enforce the condition of being 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:
        print("Not really a party")
    case 4...20:
        print("Just right")
    default:
        print("Too many people invited")
}

Here, we have described each case as a range of possible values. The first case includes all of the values between and including 0 and 3. This is way more expressive than using a where clause. This example also shows a rethinking of the logic. Instead of having a case specific for values over 20, we have cases for the closed ranges that we know and then capture everything for the case above 20 in the default case. Note that this version of the code does not properly handle the situation in which the count might be negative, whereas the original version did. In this version, if the count were -1, it would fall all the way through to the default case and print out "Too many people invited". For this use case, it is fine because the count of an array can never be negative.

Switches don't only work with numbers. They are great for performing any type of test:

switch name {
    case "Marcos", "Amy":
       print("\(name) is an honored guest")
    case let x where x.hasPrefix("A"):
        print("\(name) will be invited first")
        fallthrough
    default:
        print("\(name) is someone else")
}

This code shows some other interesting features of 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 same code for.

The second case uses a custom test on the name to see if it starts with the letter A. This is great for demonstrating the way in which switches are executed. Even though the string Amy would satisfy the second condition, this code would only print, Amy is an honored guest because the other cases are not evaluated once the first case is satisfied. For now, don't worry if you don't understand completely how hasPrefix works.

Lastly, the second case uses the fallthrough keyword. This tells the program to execute the code in the following case. Importantly, this bypasses the next case's condition; it does not 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":
        print("\(testName) is an honored guest")
    case let x where x.hasPrefix("A"):
        print("\(testName) will be invited first")
        fallthrough
    case "Jamison":
        print("\(testName) will help arrange food")
    default:
        print("\(testName) is someone else")
}

Some good names to try are Andrew, Amy, and Jamison.

Now we have full control over which code we want executed in which 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 viable to copy and paste a bunch of code. Instead, we can use control structures called loops.

Loops

There are many different types of loops but all of them 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 {
    print("\(invitees[index]) is invited")

    index+=1
}

A while loop consists of a condition to test and code to be run until that condition fails. In the preceding example, we have 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 used a new operator += which added one to the existing value. This is the same as writing index = index + 1.

There are two important things to note about this loop. Firstly, our index starts at 0, not 1, and it goes on until it is less than the number of invitees, not less than or equal to them. 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: the last element of an array is at the index one less than the count.

The other thing to note is that, if we were to forget to include index+=1 in the loop, we would have an infinite loop. The loop would continue to run forever because index would never go beyond invitees.count.

This pattern of wanting to loop through a list is so common that there is a more concise and safe loop called a for-in loop:

for invitee in invitees {
    print("\(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. One thing to note is that we did not declare the invitee variable with let or var. This is particular to a for-in loop because the constant used there is newly declared each time through the loop.

for-in loops are great for looping through different types of containers. They can also be used to loop through a dictionary, as shown:

for (genre, show) in showsByGenre {
    print("\(show) is a great \(genre) series")
}

In this case, we get access to both the key and the value of the dictionary. This should look familiar because (genre, show) is actually a tuple used for each iteration through the loop. It may be confusing to determine whether or not you have a single value from a for-in loop like arrays or a tuple like dictionaries. At this point, it would be best for you to remember just 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.

Another feature of for-in loops is the ability to only loop through elements that pass a given test. You could achieve this with an if statement but Swift provides a more concise way of writing it using the where keyword:

for invitee in invitees where invitee.hasPrefix("A") {
    print("\(invitee) is invited")
}

Now, the loop will only be run for each of the invitees that start with the letter A.

These loops are great but sometimes we 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. To do this, we can use a range similar to a Switch, as shown:

for index in 0 ..< invitees.count {
    print("\(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. This one is called a half 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:

print("Counting to 10:")
for number in 1 ... 10 {
    print(number)
}

The closed range includes the last value so that the loop will print out every number starting with 1 and ending with 10.

All loops have two special keywords that let you modify their behavior, which are called continue and break. continue is used to skip the rest of the loop and move back to the condition to see whether or not the loop should be run again. For example, if we didn't want to print out invitees whose name began with A, we would use the following:

for invitee in invitees {
    if invitee.hasPrefix("A") {
        continue
    }
    print("\(invitee) is invited")
}

If the condition invitee.hasPrefix("A") were satisfied, the continue command would be run and it would skip the rest of the loop, moving onto the next invitee. Because of this, only invitees not starting with A would be printed.

The break keyword is used to immediately exit a loop:

for invitee in invitees {
   print("\(invitee) is invited")

   if invitee == "Tim" {
       print("Oh wait, Tim can't come")
       break
   }
}
print("Jumps here")

As soon as a break is encountered, the execution jumps to after the loop. In this case, it jumps to the final line.

Loops are great for dealing with variable amounts of data, like our list of invitees. When writing your code, you probably won't know how many people will be in that list. Using a loop gives you the flexibility to handle a list of any length.

As an exercise, I recommend you try writing a loop to find the sum of all the multiples of 3 under 10,000. You should get 16,668,333.

Loops are also a great way of reusing code without duplicating it but they are just the first step towards quality code reuse. Next, we will talk about functions, which opens up a whole new world of writing understandable and reusable code.