Swift: The Good Switch of the East

plug-and-switch-1098753-m

Ding Dong, the switch isn’t dead. It lives in Swift as a practical control structure. If if-then-else statements are Radio Shack, switches are the Container Store. Switches add boutique code organization, coalescing run-on conditionals (if then else if then else if then else….) into well-organized flows.

Here’s more than you wanted to and less than you probably need to know about this terrific Swift feature.

As in other languages, switch statements enable you to branch your code. Starting with a single value, you test that value against any number of conditions. When the condition succeeds, the switch construct executes associated statements.

Basic Switches

The simplest switch statement is this. Unlike Objective-C, you do not add parentheses around the value unless the value is actually a tuple and requires parentheses for syntax.

switch value {
default: break
}

When you add a body to this default case, the switch executes that behavior, regardless of any value that is passed. A switch without case requests means “ignore the value and do whatever the default case says to do”.

switch value {
default: println("Always prints")
}

You branch by adding case bodies. Each case keyword is followed by one or more values, a colon, and then one or more statements. When the case matches, the statements execute.

For example, this switch statement adds two cases in addition to its default. When the integer value is 2 or 5, the switch prints specific messages.

switch intValue {
case 2: println("The value is 2")
case 5: println("The value is 5")
default: println("The value is not 2 or 5")
}

Switch statements must be exhaustive. If a value can pass through each case without matching, add a default statement. Default is the catch-all else of switches.

Switch uses pattern matching. The wildcard expression _ (that’s an underscore) matches anything and everything. It adds another way provide exhaustive coverage. The following is functionally equivalent to the default-only example you saw above.

switch value {
case _: println("Always prints")
}

Break

Use the break statement to create do-nothing cases or to short-circuit ongoing evaluation.  This dreadful code snippet showcases break. It ignores any value except 4 and 6.

switch (intValue) {
case 1...3: break
case 4...6: 
    if intValue == 5 {break}
    println("4 or 6")
default: break
}

Fallthrough

Unlike Objective-C, Swift switch cases do not normally fall through. In Objective-C, the break keyword ends case evaluation and prevents other cases from executing. Swift supports break but it is not required. After matching, switch normally completes its execution at the start of the next case statement.

In the following example, the second case 2 never gets executed. It is legal, however, to put it there.

switch intValue {
case 2: println("The value is 2")
case 2: println("The value is still 2, by heavens!")
default: println("The value is not 2")
}

The fallthrough keyword continues execution into the next case, regardless of whether that next case is satisfied or not. When you pass a value of 5, this next switch statements print out first 5 and then 5 or 6.

switch intValue {
case 5: println("5"); fallthrough
case 6: println("5 or 6")
case 7: println("7")
default: println("not 5, 6, or 7") 
}

Fallthrough enables you to create or statements, as in the following. The first two cases test for 5 or 6.

switch intValue {
case 5: fallthrough
case 6: println("5 or 6")
case 7: println("7")
default: println("not 5, 6, or 7") 
}

Complex Cases

It’s simpler to combine or cases by add values to a case. Just separate  items with commas. Switch uses the pattern match operator ~= to test case elements against the passed argument. This enables you to test against ranges as well as single values.

switch intValue {
case 5, 6: println("5 or 6")
case 7...12: println("7 - 12")
case 13...15, 17, 19...23: 
    println("13, 14, 15, 17, or 19-23")
default: println("Not 5-15, nor 17, nor 19-23") 
}

Pattern matching enables switch to work with many types, including strings:

let string = "Goofy"
switch string {
case "Donald", "Mickey": println("Stars")
case "Daisy", "Minnie": println("Cameos")
case "Goofy", "Uncle Scrooge", "Ludwig":
    println("Recurring")
default: println("Available for children's parties")
}

Tuples

Switch matches tuples as well as individual values. This example tests a 2-ary tuple to check for threes. The wildcard expressions in the second case match a single 3  to either position.

switch tup {
    case (3, 3): println("Two threes")
    case (_, 3), (3, _): 
        println("At least one three")
    case _: println("No threes")
}

You can also use ranges inside tuple arguments.

switch tup {
case (0...5, 0...5): 
    println("Small positive numbers")
case (0...5, _), (_, 0...5): 
    println("At least one small positive number")
default: 
    println("No small positive numbers")
}

Value Bindings

The let and var keywords bind values to temporary variables. This is where switch statements move beyond simple if-replacements and  showcase their power.

This next example is from my enumerations write-up. An initializer tests its parameter against various types to construct an enumeration instance. Each case attempts to set a value, continuing on to the case body only when that assignment succeeds. Each as cast fails until the right type is found.

extension Container {
    init() {self = .NilContainer}
    init(_ t : T){
        switch t {
        case let value as Int : 
            self = .IntContainer(value)
        case let value as Double: 
            self = .DoubleContainer(value)
        case let value as String: 
            self = .StringContainer(value)
         default: self = .NilContainer
        }
    }
}

Although the var keyword is available, it’s rarely needed. Here’s a pointless example that uses var instead of let.

switch intValue {
case var value: println(++value)
}

Where clauses

Using where adds logic to case conditions beyond pattern matching. This example tests for membership in a range and whether the number is even or odd.

switch intValue {
case 0...20 where intValue % 2 == 0: 
    println("Even number between 0 and 20")
case 0...20: 
    println("Odd number between 0 and 20")
default: 
    println("Some other number")
}

The where clause offers flexible logic. It lets you compare members of a tuple or test values against ranges or add any other condition your application demands.

switch tuple {
case (let x, let y) where x == y: 
    println("tuple items are equal")
case (0, let y) where 0...6 ~= y: 
    println("x is 0 and y is between 0 and 6")
default: break
}

Unwrapping

Switch statements are amazing when you work with optional elements. Here’s a real life example. I regularly use the following structure to store Bezier elements.

public struct BezierElement {
    public var elementType: CGPathElementType 
    public var point: CGPoint?
    public var controlPoint1: CGPoint?
    public var controlPoint2: CGPoint?
}

The point, controlPoint1, and controlPoint2 fields are all optional. Not every element type requires every field. For example, a quadratic curve doesn’t use controlPoint2.

This next switch statement excerpt tests quadratic elements by matching against a field tuple.

switch(elementType.value, point, 
    controlPoint1, controlPoint2) {
case (kCGPathElementAddQuadCurveToPoint.value, 
    let .Some(point), let .Some(controlPoint1), _): 
    ...point and controlPoint1 are both unwrapped...
}

The let .Some(x) constructs in this case statement unwrap optional fields. Not only does this switch statement match element type, all relevant field data is prepared for use. Both point and controlPoint1 are unwrapped for the case body. Best of all, this case never executes if the element is malformed and does not provide the required fields.

Although you can use compound if-let statements to achieve the same result, switch statements enable you pattern match and add tests (using where) at the same time.

As this write-up showcases, switch statements are ridiculously powerful and remarkably useful. They are simply one of the best Swift language features available.

Comments are closed.