Swift Tip: Exhaustive Switching with Enums
As you may already know, we're currently updating our book, Advanced Swift, for Swift 5. As part of the rewrite, we're adding an entirely new chapter on enums, a work-in-progress by our co-author Ole Begemann.
Below is an excerpt from this new chapter, taken from the section "Designing with Enums". It details the benefits of exhaustive switching.
For the most part, switch
is just a more convenient syntax for an if case
statement with multiple else if case
clauses. Syntax differences aside, there's one important distinction: a switch statement must be exhaustive, i.e. its cases must cover all possible input values. The compiler enforces this.
Exhaustiveness checking is an important tool for writing safe code, and particularly for keeping code correct as programs change. Every time you add a case to an existing enum, the compiler can alert you to all places where you switch over this enum and need to handle the new case. Exhaustiveness checking isn't performed for if
statements, nor does it work in switch
statements that include a default clause — such a switch
can never not be exhaustive since default
matches any value.
For this reason, we recommend you avoid default clauses in switch statements if at all possible. You can't avoid them completely because the compiler isn't always smart enough to determine if a set of cases is in fact exhaustive (the compiler only ever errs on the side of safety, i.e. it will never report a non-exhaustive set of patterns as exhaustive). We saw an example of this above when we switched over an Int8
and our patterns covered all possible values.
False negatives aren't a problem when switching over enums though. Exhaustiveness checks are one hundred percent reliable for the following types:
-
Bool
-
Enums, as long as any associated values can be checked exhaustively or you match them with patterns that match any value (wildcard or value-binding pattern)
-
Tuples, as long as their member types can be checked exhaustively
Let's look at an example. Here we switch over the Shape
enum we defined above:
enum Shape {
case line(from: Point, to: Point)
case rectangle(origin: Point, width: Double, height: Double)
case circle(center: Point, radius: Double)
}
let shape = Shape.line(from: Point(x: 1, y: 1), to: Point(x: 3, y: 3))
switch shape {
case let .line(from, to) where from.y == to.y:
print("Horizontal line")
case let .line(from, to) where from.x == to.x:
print("Vertical line")
case .line(_, _):
print("Oblique line")
case .rectangle, .circle:
print("Rectangle or circle")
}
We include two where
clauses to special-case horizontal (equal y coordinates) and vertical (equal x coordinates) lines. These two cases aren't enough to cover the .line
case exhaustively, so we need another clause that catches the remaining lines. Although we're not interested in distinguishing between .rectangle
and .circle
here, we prefer listing the remaining cases explicitly over using a default clause.
By the way, the compiler also verifies that every pattern in a switch
carries its weight. If the compiler can prove that a pattern will never match because it's already covered in full by one or more preceding patterns, it will emit a warning.
In this discussion about the benefits of exhaustiveness checking, we have assumed that enums and the code that works with them evolve in sync, i.e. every time a case is added to an enum, the code that switches over that enum can be updated at once. This is usually true if you have access to the source code of your program's dependencies and the program and its dependencies are compiled together. Things become more complicated when libraries are distributed as binaries and the programs using them must be prepared to work with a newer version of the library than was known when the program was compiled. In this situation, it can be necessary to always include a default clause even in otherwise exhaustive switches. We'll come back to this in the section "Frozen and Non-Frozen Enums" later in this chapter.
We're aiming to release the new edition of Advanced Swift at the beginning of May. The update will be free for all existing Ebook readers!