OK, thanks. It looks like throw
doesn't get that treatment, though, from @Jens' comment.
It seems useful / reasonable being able to do something like:
var someCondition = true
func someFunction() {
defer {
print("Doing something deferred before guard.")
guard someCondition else { exitScopeOfDeferStatement }
print("Doing something deferred after guard.")
}
print("Doing something in someFunction.")
}
Especially since the following program compiles and works as expected:
var someCondition = true
func someFunction() {
defer {
workaroundScope: do {
print("Doing something deferred before guard.")
guard someCondition else { break workaroundScope }
print("Doing something deferred after guard.")
}
}
print("Doing something in someFunction.")
}
someFunction()
I don't see why the scope of a defer statement should be the only (?) scope in which guard statements can't be used (without a workaround like this).
The second example works because it is the do {}
scope which the guard
is exiting, not the defer
.
[edit]
That it requires the label to avoid the compiler error is probably a bug, though.
Having a way to break out of a guard
inside a defer, without needing an intermediate do
block, does seem useful.
Or rewriting to use an if {}
statement.
Would it be reasonable to allow break
to exit a defer
block?
Maybe. My concern with that is that people might expect break
in for ... { defer { break } }
to exit the outer loop.
Is there any way to get actual data on that? It seems obscure enough that it should be fine if it's properly documented, but I'd rather not rely on speculation.
It's hard to gather data on a feature that doesn't exist I don't believe a survey would reach a representative audience. For what it's worth, I would only expect
break
to exit the current scope, which would be the defer
.
We could allow the argument to break
/ continue
to be a statement keyword instead of a label, e.g. break for
or break switch
. That has some nice readability advantages when e.g. you just have a switch
inside a loop and you want to break out of the loop, because anybody reading it knows what break for
means without having to remember a label (as well as without requiring the original programmer to come up with a label in the first place). break defer
would then be a natural extension of that. And it would be okay that you can't label a defer
because you can never break out of an outer defer
anyway.
I think we need clear and complete documentation of Swift's control statements before we can come up with a sensible solution, because it seems like they are currently both poorly documented and poorly understood.
This thread shows that the intended behavior of defer and guard is not clear. And the workaround that I mentioned above is relying on undocumented behavior, as are the examples I will show below.
For reference, here are some quotes from The Swift Programming Language (4.1), describing labeled statements, do
, break
and continue
:
Labeled statement
You can prefix a loop statement, an if statement, a switch statement, or a do statement with a statement label, which consists of the name of the label followed immediately by a colon (:). Use statement labels with break and continue statements to be explicit about how you want to change control flow in a loop statement or a switch statement, as discussed in Break Statement and Continue Statement below.
The do statement
The do statement is used to introduce a new scope and can optionally contain one or more catch clauses, which contain patterns that match against defined error conditions. Variables and constants declared in the scope of a do statement can be accessed only within that scope.
A do statement in Swift is similar to curly braces ({}) in C used to delimit a code block, and does not incur a performance cost at runtime.
/... some more catch-related info .../
The break statement
A break statement ends program execution of a loop, an if statement, or a switch statement. A break statement can consist of only the break keyword, or it can consist of the break keyword followed by the name of a statement label, as shown below.
- break
- break label name
When a break statement is followed by the name of a statement label, it ends program execution of the loop, if statement, or switch statement named by that label.
When a break statement is not followed by the name of a statement label, it ends program execution of the switch statement or the innermost enclosing loop statement in which it occurs. You canât use an unlabeled break statement to break out of an if statement.
In both cases, program control is then transferred to the first line of code following the enclosing loop or switch statement, if any.
The continue statement
A continue statement ends program execution of the current iteration of a loop statement but does not stop execution of the loop statement. A continue statement can consist of only the continue keyword, or it can consist of the continue keyword followed by the name of a statement label, as shown below.
- continue
- continue label name
When a continue statement is followed by the name of a statement label, it ends program execution of the current iteration of the loop statement named by that label.
When a continue statement is not followed by the name of a statement label, it ends program execution of the current iteration of the innermost enclosing loop statement in which it occurs.
In both cases, program control is then transferred to the condition of the enclosing loop statement.
In a for statement, the increment expression is still evaluated after the continue statement is executed, because the increment expression is evaluated after the execution of the loopâs body.
As far as I can see, it is not clear from the documentation that break
and continue
will work with a (labeled) do
statement.
The following (command line) example programs, which compiles successfully with Xcode 9.3, demonstrates that --- and how --- continue
and break
work with do
:
This program:
var count = 0
usingDoAsALoop: do {
count += 1
print("A:", count)
if count < 3 { continue usingDoAsALoop }
print("B:", count)
}
Will print:
A: 1
A: 2
A: 3
B: 3
And this program:
var count = 0
usingDoAsALoop: do {
count += 1
print("A:", count)
if count >= 3 { break usingDoAsALoop }
print("B:", count)
continue usingDoAsALoop
}
Will print:
A: 1
B: 1
A: 2
B: 2
A: 3
I think it's important to be able to break
(make an early exit from) a do
statement, but I'm not sure we should be able to continue
a do
statement, because I agree with the following error messages, that a do
statement is not a loop:
// This is the previous example program with the label removed:
var count = 0
do {
count += 1
print("A:", count)
if count >= 3 { break } // Error 1
print("B:", count)
continue // Error 2
}
Error 1: Unlabeled 'break' is only allowed inside a loop or switch, a labeled break is required to exit an if or do
Error 2: 'continue' is only allowed inside a loop
Error 1 confirms our findings, that break
is intended to be allowed in a do
statement, but only if it is labeled. This is fine (but should be better documented).
Error 2 seems to imply that a do
statement simply is not a loop. But the previous two example programs demonstrated that continue
is allowed within a (labeled) do
statement, and that this turns the do
statement into a loop ...
I agree that it looks like the official documentation is missing some of the expressivity of labelled break
/continue
. That's worthy of a bug, if you'd like to file it. I don't think it changes anything about my suggestion, though.
Would the core team find it reasonable to allow conditionally exit out the defer body, is it worth pushing forward?
I'd just like to be sure about the intended behavior. For all I know (given the current state of the documentation and actual behavior), there could be some intended way of making an early exit from a defer body, that simply isn't working.
Would the core team find it reasonable to allow conditionally exit out the defer body, is it worth pushing forward?
You don't really have to ask this question; if the community feels strongly that something is a problem, that's always something the core team should consider.
In this case, I feel comfortable saying there's also core-team support for treating the inability to early-exit from defer
as a defect, since both Joe and I have weighed in about it.
Iâd just like to be sure about the intended behavior. For all I know (given the current state of the documentation and actual behavior), there could be some intended way of making an early exit from a defer body, that simply isnât working.
That's fair. No, there isn't a direct way of early-exiting from defer
, although of course there are workarounds.
Well I don't want to rush and make mistakes I did in the past, so it is always good to ask first. Thank you for clarification though.
Filed SR-7708 for the labeled do statement working like a loop with continue, as I guess that is a bug.
Regarding the current lack of a (nice) way of early-exiting from defer
, I guess someone should write a pitch for that.