Can I choose what block a `defer` statements belongs to?

Suppose I have a function like this:

func disposeController(_ controller: Controller) {
    let requiresDisconnection;
    if controller.isConnected {
        requiresDisconnection = true
        gameManager.prepareHandoff()
    } else {
        requiresDisconnection = false
    }
    controller.shutdown() // clobbers .isConnected
    if requiresDisconnection {
        gameManager.disconnect(controller.id)
    }
}

I would ideally formulate the function similar to this:

topLevel: func disposeController(_ controller: Controller) {
    if controller.isConnected {
        gameManager.prepareHandoff()
        defer topLevel {
            gameManager.disconnect(controller.id)
        }
    }
    controller.shutdown()
}

Is there any syntax that allows something like this?

1 Like

Of course the example presented here is poor API design. But I could imagine something like that really shipping somewhere.

The defer version would also have the behavioral difference that if shutdown() throws, the defer runs. I can imagine that this could be construed as undesirable but I can also imagine cases where it might be desirable.

No. See Targeting a specific scope with defer.

let requiresDisconnection: Bool
defer { 
  if requiresDisconnection {
    gameManager.disconnect(controller.id)
  }
}
2 Likes

I don't think it is. This pattern crops up in all sorts of places naturally. Swift is better than some other languages at it - at least with the manual let actuallyDoTheThing: Bool Swift will ensure it's properly initialised on every code path - but not ideal.

is there an underlying reason why gameManager.disconnect(controller.id) cannot come before controller.shutdown()?

1 Like

No, which is why I call this snippet poor API design.

Here is a more realistic example

// For a persistent connection, call .initialize(), then .connect()
// For a transient connection, call .connect() directly
class Connection {
    func connect(to target: Target) throws(ConnectionError) {
        let transient: Bool
        if !self.initialized {
            self.initialize()
            transient = true
        } else {
            transient = false
        }
        defer {
            if transient {
                self.deinitialize()
            }
        }
        try dispatchConnection(via: self, to: target)
    }
}
1 Like

There have been times when I've wanted something like this, but then I consider that:

  1. I'd need to add a label-able scope for the defer to run, and figure out a clear name for it.

  2. It's actually nice that defer only applies to the immediate scope it's in. It means that when I see a scope, I can easily tell which defers will run at its end, and I don't need to consider all nested scopes and whatever conditions they might have.

So ultimately, something like:

is probably the best thing. It's very easy to understand. I appreciate that it can be difficult to think of names for variables, but you'd also need to think of a name for the scope label so there's not really any difference.

As for throwing, that also sounds like behaviour which is better written out explicitly.

Clear code is beautiful code. In your example, you have a transient boolean, reflecting whether or not the connection is transient. Great - easy to understand. Why change it?

I mean, you could do this to shorten it a bit, but the overall structure and placement of the defer is fine IMO:

let transient = !self.initialized
if transient {
    self.initialize()
}
defer {
    if transient {
        self.deinitialize()
    }
}
try dispatchConnection(via: self, to: target)
4 Likes

I don’t think the argument that it’s easier to see what defers run is persuasive since we can guard defers behind conditions with if or guard. Consider:

func bar(_ condition: Bool) {
    guard condition else {
        print("condition was false")
        return
    }
    defer {
        print("condition was true")
    }
    return
}

defer blocks inside conditional scopes still execute at the end of that scope, not at the end of their parent scopes. For instance:

func test(_ condition: Bool) {
    if condition {
        defer { print("Deferred work") }
    }
    print("Main body")
}

test(true)
// Prints:
// Deferred work
// Main body

Indeed, the compiler emits a warning for the above code, telling you as much:

2 |     if condition {
3 |         defer { print("Deferred work") }
  |         `- warning: 'defer' statement at end of scope always executes immediately; replace with 'do' statement to silence this warning
4 |     }

If it were possible for a deeply nested defer to attach itself to one of its parent scopes, it would mean that, if I'm trying to understand the flow of execution of that parent scope, I would need to go hunting around in all the child scopes.

With defer as it currently is, I only need to consider those defers which are immediate children of the scope I'm trying to understand.

Yes, that’s not what I was talking about. I was saying that it’s already possible to make a defer block that only gets scheduled if some condition was met by exiting the main scope early if that isn’t the case. This means you already might have to go hunting throughout the scope to find these defer blocks. Admittedly you will only find them in places where the control flow would’ve exited the main scope otherwise, and there is no path of execution lexically beyond the defer block where the defer block isn’t going to run, but you still don’t have the ability to immediately see what defer blocks are scheduled to run at the end of a block. If we wanted that clarity we’d have to enforce that defer blocks only occur at the start of a scope.

2 Likes