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
,continue
is specific to theswitch
statement. You must use labels to break or continue with respect to a surrounding loop. - Both
continue
andfallthrough
allow execution to continue to successive cases, however their intent is distinct.fallthrough
moves forward without testing, automatically matching the next case.continue
moves forward but will not execute until it finds a matching case. Because of this, I believefallthrough
andcontinue
states 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