In previous posts we’ve talked about many of Swift’s control flow statements. We’ve covered the if-else
statement, the guard
statement as well as the break
, continue
and loop statements. In this article, we’re going to complete our set by looking at the one control flow statement that we haven’t touched on yet – Swift’s switch
statement.
Table of Contents
The Switch Statement
In Swift, the switch
statement, like in other languages, is a branching statement that allows us to execute one of a number of different paths through our code. In this way, it is similar to the if-else
statement but where the if-else
statement is suited for simple control flow with relatively few potential paths, the switch
statement allows us to construct much more complex control flow with many potential paths through our code. As such, the switch
statement provides a useful alternative to nested or chained if
statements and often makes our code both cleaner and easier to understand.
So how does the switch
statement actually achieve this complex branching? Well at it’s core, the main driver behind the switch
statement is pattern matching.
The Swift language has an extremely powerful pattern matching mechanism built in, a pattern matching mechanism that the switch
statement uses to sequentially compare a value (known as the switch value) to a series of different cases. The switch statement then executes the code block associated with the first match that it finds. Once the code block has finished executing, execution then transfers (by default) to the first statement after the closing curly brace of the switch
statement. This is known as switching on the switch value.
So, we’ve talked about it, but what does a switch
statement actually look like in code?
Well, if you’ve done any programming in other C-based languages before, the basic syntax for the switch
statement in Swift will be relatively familiar to you:
switch <switchValue> {
case <pattern 1>:
<code block for pattern 1>
case <pattern 2>:
<code block for pattern 2>
default:
<code block that will execute if no prior matches>
}
As with many other languages, in it’s simplest form, the switch
statement starts with the switch
keyword and is then followed by a switch value. Unlike in other languages this switch value doesn’t need to be wrapped in parentheses unless it’s a tuple though. After that we then have an opening curly brace ({
). The opening curly brace denotes the start of the body of the switch
statement. Within the body of the switch statement there are then a series of cases against which the switch value is compared.
Cases
Each case in a switch
statement represents one potential path through our code.
Each case
can be either an explicit case (a case that encompasses a pattern against which to match the switch value) or a default case (that doesn’t have a pattern but will match if none of the previous cases have matched).
Switch Statement Explicit Cases
For explicit cases, each case
begins with the case
keyword. The case
keyword is then followed by one or more comma-separated patterns, a colon and then a code block containing one or more statements. The colon is used to separate the cases pattern(s) from the cases code block.
In their most basic form each pattern in a case
is a value (also known as an identifier pattern (we’ll talk more about identifier patterns in a future post) against which to compare the switch value. These values can be anything – the integer 42
, the string Hello
or much even more complex compound values such as tuples.
Where the switch
statement in Swift differs from similar statements in other languages though, is that these patterns can also extend far beyond these simple value matching uses.
In Swift the very same pattern matching mechanism we talked about just now also encompasses much more complex patterns that we can use to match composite values such as tuples, patterns to match optional types, patterns to compare values against ranges and even patterns that allow us to bind matched values to new constants and variables.
We’ll look at all of these pattern matching options in a future post but for now, let’s focus on the switch
statement itself and look at the second type of case in a switch
statement – the default case.
Switch Statement default
Case
As we’ve seen, the body of Swift’s switch
statement can contain one or more cases against which the switch value is compared but in addition to these explicit cases, the switch
statement also supports the inclusion of a default
case.
When included, the default
case must be written as the last case within a switch
statement and is written using the default
keyword followed by a colon and then a code block containing one or more statements. The default
case is a kind of wild-card case which will automatically match a switch value if none of the other previous cases within the switch
statement have matched.
The simplest form of switch
statement we can therefore write is:
switch value {
default:
break
}
Now, apart from being a default case that matches if none of the previous cases match the default
case also plays a more important role than you may be used to from the other languages. This is for one primary reason – in Swift, the cases of a switch statement must be exhaustive.
So what does that mean?
Well, for any switch value of a given type that is considered within the switch
statement, the cases in the switch
statement must cover ALL potential values that that type can take. So for example, if we had an Int
value as the switch value, the cases in the switch
statement must cover all potential Int
values (of which there are obviously a lot!). Similarly, if the switch value were a String
then all possible String
values would need to be covered.
The reason Swift’s language designers took this approach is to ensure that bugs don’t creep into our code where we are considering a switch value and none of the cases match. This adds a lot of safety but does leave us with a problem: How do we write explicit cases for every potentially value. It simply isn’t feasible. Unless we use the default
case.
So it is for this reason that the default
case of a switch
statement in Swift is slightly more important than it would be in other languages. It provides a catch-all case that allows us to ensure all possible values of a type are covered without having to write a whole bunch of explicit cases. In fact, if you forget to include the default
case in your switch
statement and the previous explicit cases don’t collectively cover all the potential values of the switch value, the Swift compiler will raise an error and demand that you include a default
case.
Ok, that’s the different types of switch
case, but what about the code blocks associated with each of those cases?
Code Blocks
As I’ve mentioned previously, each case in a switch
statement has it’s own associated code block. The code block in a switch
statement is analogous to a branch in an if-else
statement and represents the code that will be executed if that particular case matches.
In Swift, the code block of a particular case can contain anything we like but must contain at least one executable statement. This means that we can’t have empty code blocks and we also can’t have blocks that contain just code comments as they also aren’t executable statements.
For example, the following is illegal in Swift due to the first case having no statements within it’s associated code block:
switch <valueBeingConsidered> {
case <value1>:
case <value2>:
<statements>
default:
<statements>
}
In this case, the compiler will raise an error indicating that the first case did not contain any executable statements.
But this leaves us with a problem. What if we wanted to simply ignore certain values in a switch
statement? Say I had a switch value that was an integer and only wanted to do something if the integer had a value of 1
or 2
but didn’t want to execute anything otherwise? What would we put in our default
case here?:
let diceRoll = Int(arc4random_uniform(6) + 1)
switch diceRoll {
case 1:
print("It's 1!")
case 2:
print("It's 2!")
default:
// What do we put here?
}
We’ve already seen that our case statements need to be exhaustive so we have to include a default
case, but we don’t actually want to execute any code in this case. This is where the break
statement comes in.
Ignoring Cases Using the break
Statement
As we’ve seen in a previous post the break
statement can be used to immediately end the execution of a loop, an if-else
statement, a do
statement or a switch
statement by breaking out of that statement and transferring execution to the first statement immediately after the statements closing curly brace (}
).
When used in conjunction with a switch
statement, we can use the break
statement, to ignore particular cases by having the break
statement as the sole statement within a cases code block. By doing this we solve two problems. Firstly, the cases code block has at least one executable statement (the break
statement) and secondly, should the case match, the break
statement causes execution to transfer to the first statement after the closing curly brace of the switch
statement essentially ignoring the matched value. Great.
Now, there are a few other situations we should also look at before we close things out, the first is how we match multiple potential values and execute the same code block for each.
Cases Matching Multiple Values
Imagine the scenario, you want to write a switch
statement where the same block of code needs to be executed for more than one value switch value. Say it was a dice role, and we wanted to print out the face that the role was odd or even (a slightly contrived example but go with it for now).
We could write something like this:
let diceRoll = Int(arc4random_uniform(6) + 1)
switch diceRoll {
case 1:
print("Odd")
case 2:
print("Even")
case 3:
print("Odd")
case 4:
print("Even")
case 5:
print("Odd")
case 6:
print("Even")
default:
break;
}
But that’s a lot of duplicate code for not much benefit. As an alternative then, we could turn this into a more compact form by supplying multiple, comma-separated, patterns for each case:
let diceRoll = Int(arc4random_uniform(6) + 1)
switch diceRoll {
case 1, 3, 5:
print("Odd")
case 2, 4, 6:
print("Even")
default:
break;
}
In this version, the switch
statement will compare the switch value to each of the patterns listed in the case and if any one of them matches, the code block associated with that case will execute. It’s a sort of short-hand form that stops us having to write the same code blocks in multiple places as we saw in the previous example. In addition, if we had a lot of different patterns, we can either write all of the patterns for a particular case can be written on a single line as I’ve done here, or can also be written over multiple lines if necessary. It just helps with making our code a bit clearer.
Ok, we’re pretty much there, just one more thing I wanted to cover today and that is the concept of fall through.
Fall-through
If you’ve ever worked with switch
statements in other languages, you’re probably familiar with the concept of fall through.
Fall through is where the execution path in a switch
statement, matches a particular case, executes the code block associated with that case and then falls through to execute the code block of the next case as well.
In other C-based languages, this actually the default behaviour and usually necessitates adding a break
statement to the end of each cases code block to prevent this behaviour thereby ensuring execution continues after the switch
statements closing brace.
Swift however, is different. You may have already noticed the absence of break
statements in each of the switch
cases we’ve looked at so far (with the exception of the one specific case we just looked at). This is because in Swift, the cases of a switch
statement do NOT fall through by default. Instead Swift’s language designers decided to take the opposite approach, opting to eliminate the often tricky-to-track-down bugs due to missing break
statements in preference to the predictability afforded by cases not falling through automatically.
For example in following code example, the code block of the second case is never executed due to the switch value matching the first case and then execution continuing after the closing curly brace of the switch
statement. (This code might look strange but it is actually perfectly valid in Swift – though you’d probably never write it):
let value = 2
switch value {
case 2:
print("Hello")
case 2:
print("World")
default:
break
}
print("Switch Ended")
// Hello
// Switch Ended
Now, with everything I’ve said above, it is not to say that fall through is impossible in Swift. There are actually some valid situations where fall-through is a useful concept so to account for this, the Swift language also supports the fallthrough
statement.
The fallthrough
statement allows us to opt-in to the fall-through behaviour seen in other C-based langauges on a case-by-case basis. It’s a mechanism that lets us indicate to Swift, that we want execution to continue with the code blocks of subsequent cases in the switch
statement and allows us to force the execution of those code blocks regardless of whether the patterns in those cases match the switch value.
In Swift, the fallthrough
keyword has very specialised usage though. You can only use it within the body of a switch
statement and even here, it cannot be used within the last code block of a switch
statement nor in cases where the next case of the switch
statement contains a value binding pattern (we’ll look at value-binding patterns in a future post).
Another thing to note is that when you do use the fallthrough
statement, the statement doesn’t have to be the last statement in the code block (we’ll see an example of this shortly) either and when executed, causes execution to be transferred directly to the statements within the code block of the next case potentially skipping any statements that immediately follow it.
When execution is transferred to the new code block, the pattern matching we’ve talked is also skipped for the new case, and instead simply jumps to the statements associated with that case and starts executing them, regardless of whether that case matches the switch value or not.
Let’s have a look at another example. For this, we’ll modify our previous example slightly. This time, we’ll add our fallthrough
keyword to the first case and follow it with a print
statement. We’ll also modify the pattern for the subsequent case to a value that doesn’t match our switch value:
let value = 2
switch value {
case 2:
print("Hello")
fallthrough
print("Something else")
case 3:
print("World")
default:
break
}
print("Switch Ended")
// Hello
// World
// Switch Ended
Notice the output? This time, the switch
statement matches our switch value of 2
to the first case and executes the associated code block. This involves first printing out the word Hello
before execution hits the fallthrough
statement.
As we’ve just discussed, the fallthrough
statement causes execution to immediately jump to the code block of the next case. In this case this means it skips the print
statement that printed out the words Something else
and instead continues with the code within code block of the second case even though our switch value (2
) doesn’t match the pattern for that case (3
). Get the idea?
Ok, there we have it. Swift’s switch
statement. I’m going to leave it there for today but in a future post, we’ll take a deeper dive into the different kinds of pattern that you can use in conjunction with the switch
statement cases. Until then, if you have any questions, observations, feedback or if I’ve missed anything or got anything wrong then please get in touch and if you’ve found this useful, I’d really appreciate it if you could share it with your friends.