A few questions about `defer`s behavior

I just wanted to write something like defer { $0.someMember.doSomething() } but got an error Anonymous closure argument not contained in a closure. It made me wonder if the body of defer is considered to be a closure rather then just a different nested scope of code execution just like the else body of a guard is.

  • Is this intended behavior or a bug?

This is a bug. The defer body is treated like a closure as an implementation detail but that's not supposed to impact the user model.

9 Likes

Perhaps I'm reading the message wrong, but it seems to me that the implementation isn't impacting the user model here. The message is saying that you've used an anonymous closure argument ($0) and it isn't contained in a closure. This is the same error message you get when you write $n anywhere outside of a closure.

1 Like

The defer might very well be inside a closure, e.g. in h below:

Welcome to Apple Swift version 4.1.1 (swiftlang-902.0.50 clang-902.0.39.1). Type :help for assistance.
  1> let f = { print("Hello,", $0) }
f: (Any) -> () = 0x0000000101710070 $__lldb_expr2`closure #1 (Any) -> () in __lldb_expr_1 at repl.swift:1
  2> f("world")
Hello, world
  3> let g = { defer { print("world") }; print ("Hello,") }
g: () -> () = 0x00000001017104a0 $__lldb_expr6`closure #1 () -> () in __lldb_expr_5 at repl.swift:3
  4> g()
Hello,
world
  5> let h = { defer {print($0)}; print("Hello,")}
error: repl.swift:5:24: error: anonymous closure argument not contained in a closure
let h = { defer {print($0)}; print("Hello,")}
                       ^

1 Like

Okay, I understand now. I didn't realise that the original post was implying that the defer was inside a closure.

5 Likes

My case was an escaping closure for a subscription to an observable of RxSwift.

.subscribe(onNext: {
  defer {
    $0.doSomething() // This has to be executed at the end of any exit path
  }
  guard condition else { return }
  doOtherStuff()
})

@Joe_Groff speaking of bugs, is it also a bug that a guard is disallowed in defer?

let condition = true

let closure = {
  defer {
    // error: 'return' cannot transfer control out of a defer statement
    guard condition else { return }
    // do something here
  }
  // [...]
}

This forced me to create another closure and to path that into the guard body to workaround this issue. However this feels like another bug to me. I'll file some issue tickets if you can confirm that it's a bug.

Not being able to use guard in a defer makes some sense to me, as by definition you are already returning when the defer is executing.

2 Likes

Well defer does not return anything, it will just execute the deferred code block at the end of any execution path. I mean guard was added to Swift to resolve the issue with nested scopes that if else created. In the example above I used guard in the exact same sense to no further nest code into another scope.

Furthermore in the exact same sense we can nest guard into another guards else body which is also returning.

let condition = true
let closure = {
  // This is just a simple example.
  guard condition else {
    guard condition else {
      return
    }
    return
  }
}

The real use case I had is to execute the same code block from any exiting code path while the code that the defer will execute is guarded by a condition. If I can use if else in a defer then I should also be able to use a guard else there.

// This code snipped is from my code base (changed a little for simplicity)
.drive(onNext: { [unowned self] value in
  // `<-` is an operator that will path the `lhs` class instance
  // Into a trailing closure as first parameter
  self.viewController.intervalometer <- {
    // Workaround the first issue (error when using `$0` inside `defer`)
    let intervalometer = $0
    // Workaround the second issue (error when using `guard` inside `defer`)
    let updatePicker = {
      guard
        let setting = intervalometer.getSetting(),
        // There are also different settings other then `duration` 
        // and `interval` 
        setting == .interval || setting == .duration
      else { return }
      // Reload only a specific picker
    }
    defer { updatePicker() }
    // Prevent an ugly glitch caused by integer division 
    // due to lost precision
    guard currentValue != value.newValue else { return }
    // Updateh with new value here
  }
})

Ideally I would want to write it like this:

.drive(onNext: { [unowned self] value in
  self.viewController.intervalometer <- {
    defer { 
      guard
        let setting = $0.getSetting(),
        // There are also different settings other then `duration` 
        // and `interval` 
        setting == .interval || setting == .duration
      else { return }
      // Reload only a specific picker
    }
    // Prevent an ugly glitch caused by integer division 
    // due to lost precision
    guard currentValue != value.newValue else { return }
    // Updateh with new value here
  }
})

Nested guards are different because the return statement of the outer guard has not executed. The code of the defer block is never executed until after an (implicit) return is executed.

I'm not saying the compiler can't allow it. I only think that the current behavior makes sense.

Sure I see you point, but I think returning from a defer should only mean that you want to exit out the deferred block earlier based on specific condition. I don't see any harm in this model.

1 Like

I see no harm, either. But if there's sense in the current situation, it's not necessarily a bug. That's all.

1 Like

Yeah I was just wondering that, but I understand now that it's just a historical restriction that we can potentially lift in the future right?

1 Like

Maybe it's worth pointing out that a defer statement is executed at the end of the scope in which it is defined, not the function body, so for example:

func foo() {
    defer { print("A") }
    for i in 0 ..< 3 {
        defer { print("B \(i)") }
        print("C \(i)")
    }
}
foo()

Will print:

C 0
B 0
C 1
B 1
C 2
B 2
A

From The Swift Programming Language:

A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.

A guard statement is used to transfer program control out of a scope if one or more conditions aren’t met.

3 Likes

I did not know that! I'm not certain that it should affect whether a return should be allowed. Perhaps it makes sense if it's conditional, but would seem to be poor practice if it's not.

The following compiles:

let condition = true

let closure = {
    defer {
        hmm: do {
            guard condition else { break hmm }
            // do something here
        }
    }
    // [...]
}
4 Likes

That is a weird workaround, but it's definitely good to know.

I think that it should be possible to do this without the workaround. But I'm not sure if allowing return (without an explicit return value) would be unproblematic.

From TSPL again:

The else clause of a guard statement is required, and must either call a function with the Never return type or transfer program control outside the guard statement’s enclosing scope using one of the following statements:

  • return (function)
  • break (loop, if, switch)
  • continue (loop)
  • throw (function)

So maybe the question is:

Is there any way to transfer control out of a defer statement?

Btw, here's another way in which it is visible to the user that the defer body is implemented as a closure:

import Foundation

func foo() throws {
    defer { throw NSError() } // Error: Error is not handled because the
                              // enclosing function is not declared 'throws'
    return
}
5 Likes

It isn't normally useful to transfer control out of a defer statement, since by definition you're already on the way out of whatever scope you're in, but we ought to diagnose attempts to do so as being invalid within a defer rather than providing closure-oriented error messages like this.

1 Like

The error is error: MyPlayground.playground:84:13: error: 'return' cannot transfer control out of a defer statement. At least for return and break. So it's not referring to any closures. Only the attempt to throw has the problem.