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.
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.
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,")}
^
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.
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.
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()
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.
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
}
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.
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.