A few questions about `defer`s behavior

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).

1 Like

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.

5 Likes

Or rewriting to use an if {} statement.

Would it be reasonable to allow break to exit a defer block?

1 Like

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 :slight_smile: 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.

4 Likes

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 ...

3 Likes

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.

3 Likes

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.

1 Like

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. :slight_smile:

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.