This proposal completes the switch statement's control flow transfer suite by introducing continue. This change adds functionality that many developers expect (but do not get) from fallthrough.
At this time, Swift does not allow pattern matching and execution to continue within the scope of the current switch statement. A case can fallthrough to execute the next clause but it cannot resume pattern matching. The fallthrough executes the next case clause regardless of whether the value matches its pattern or not.
switch value {
case pattern:
// ... handle pattern
fallthrough
case another pattern:
// ... execute code for both "pattern" and "another pattern"
}Fallthrough does not enable you to choose between disjoint outcomes, like those shown in the following example:
switch value {
case pattern:
// ... code to handle this case
**do something here to continue pattern matching**
case pattern where condition A:
// ... additional code where A holds
case pattern where condition B:
// ... additional code where B holds
case pattern where condition C:
// ... additional code where C holds
...:
default: break
}Introducing continue means "resume pattern matching after executing this case clause". It offers more nuanced control than currently exists within switch. Continuing has several advantages:
- It minimizes redundant code by permitting multiple pattern matches.
- It differentiates cases where code applies to one condition and not others without requiring branching within a single case clause.
- It uses an existing control flow transfer keyword that's philosophically in-step with how the keyword is used in other parts of the language.
- It works around scenarios where control should transfer but additional variables need to be bound.
To provide an example that differentiates fallthrough behavior from continue, consider "Fizz Buzz". You cannot implement this algorithm in Swift with a switch statement using fallthrough. "Fizz" and "Buzz" print for every number value, regardless of whether that number is divisible by 3 or 5.
switch value
{
case _:
print("\(value) ", terminator: "")
fallthrough
case _ where value % 3 == 0:
print("Fizz", terminator: "") // prints for every number
fallthrough
case _ where value % 5 == 0:
print("Buzz", terminator: "") // prints for every number
fallthrough
default:
print("")
}With continue, FizzBuzz works as expected, only executing matching cases while allowing control flow to continue down each successive case.
switch value
{
case _:
print("\(value) ", terminator: "")
continue
case _ where value % 3 == 0:
print("Fizz", terminator: "") // prints if divisible by 3
continue
case _ where value % 5 == 0:
print("Buzz", terminator: "") // prints if divisible by 5
continue
default:
print("")
}In some cases, the differentiation between fallthrough and continue is less clear-cut, as in the following example. This code raises an error because fallthrough cannot transfer control to a case label that declares variables, whereas continue easily could:
func myFunction(point: CGPoint) {
switch (point.x, point.y) {
case (let x, _) where x > 10:
// ... Handle x
fallthrough // error
case (_, let y) where y > 10:
// ... Handle y
case (let x, let y) where x <= 10 && y <= 10:
print("the point is too close from the border")
}
}Using continue resolves this limitation.
In Swift, fallthrough does not mean: "execute this case and then continue pattern matching", which is what many naive users expect. Given the following code where x is 5, they anticipate the function to print "5" and then "anything else". This is wrong. Swift prints "5" and then "6". Introducing continue would allow the desired "continue pattern matching" behavior.
func test(x : Int) {
switch x {
case 5:
print("5")
fallthrough
case 6:
print("6")
default:
print("anything else")
}
}Those coming from C-like languages might have the insight to expect
(wrongly, it should be noted) "5", then "6", then "anything else", which
is what you'd get with the following flawed C-ish code, where case
statements are missing break.
int x = 5;
switch (x) {
case 5: NSLog(@"5"); // no break;
case 6: NSLog(@"6"); // no break;
default: NSLog(@"anything else");
}
Swift's fallthrough statement means "continue by executing the code
defined in the next case clause". It is best suited for executing sieves where the most restrictive conditions execute specialized code and then execute code for less restrictive conditions, and so on:
case simple where even more subconditions hold: ... do complex things ...; fallthrough
case simple where subconditions hold: ... do other things ...; fallthrough
case simple: ... do base things ...
Because of its limited utility, Swift Evolution discussed removing fallthrough on-list in early December. At that time, the list arrived at a consensus that fallthrough offers sufficient utility to retain the feature in the language. fallthrough has at least one solid use-case, which is
demonstrated in this example.
While switch statements allow users to break execution, return or throw from scope, they do not allow control flow to continue to additional pattern matches. The only option available uses fallthrough. In the current design, switch statements support the following subset of control flow transfer:
control-transfer-statement → break-statement
control-transfer-statement → fallthrough-statement
control-transfer-statement → return-statement
control-transfer-statement → throw-statement
Notably missing is "continue", which this proposal would adopt.
control-transfer-statement → continue-statement
The definition of continue in a switch statement would mean "after executing the
statements in a matching case clause, continue pattern matching the remaining
cases until a match or default is found."
- Like
break,continueis specific to theswitchstatement. You must use labels to break or continue with respect to a surrounding loop. - Both
continueandfallthroughallow execution to continue to successive cases, however their intent is distinct.fallthroughmoves forward without testing, automatically matching the next case.continuemoves forward but will not execute until it finds a matching case. Because of this, I believefallthroughandcontinuestates should be mutually exclusive. When a potential execution trace through a case clause allows both strategies to trigger (i.e. notif x { continue} else { fallthrough}), the compiler should emit an error.
Rob Mayoff asks: "To be clear, under your proposal, what does the following program print?"
var x = 1
switch x {
case 1:
print("one")
x = 2
continue
case 2:
print("two")
default:
break
}My interpretation would be that the an expression passed to a switch statement is evaluated once and that value used for pattern matching:
// switch-statement → switch expression {switch-cases*}
// e.g.
switch 2 + 3 {
case 5: print("Five!")
default: break
}I'd expect this to print "one" and not "one", "two". I defer to the core team and will happily update this section should they disagree with this interpretation.
A switch statement within a loop may be broken by the introduction of continue semantics. To fix, the loop must be labeled and the continue must use that label to differentiate between switch continuation and loop continuation. This exactly matches the annotation of break with respect to loops when used in switch statements.
Not adopting this idea