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?
Joe_Groff
(Joe Groff)
2
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
tim1724
(Tim Buchheim)
4
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.
Avi
7
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
}
})
Avi
9
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
Avi
11
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
Jens
13
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
Avi
14
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.
Jens
15
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.
Jens
17
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?
Jens
18
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
Avi
21
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.