Swift's defer Statement is Funkier Than I Thought





5.00/5 (1 vote)
Swift's defer statement is funkier than I thought
Swift 2.0 introduced the defer
keyword. I've used this a little but only in a simple way, basically when I wanted to make sure that some code would be executed regardless of where control left the function, e.g.
private func resetAfterError() throws
{
defer
{
selectedIndex = 0
isError = false
}
if /* condition */
{
// Do stuff
return
}
if /* other condition */
{
// Do other stuff
return
}
// Do default stuff
}
In my usage to date, there has always been some code that should always be executed prior to the function's exit and additionally only one piece of code. Therefore, I've always put the defer
statement at the top of the function so when reading, it's pretty obvious.
I was aware that if there were multiple defer
statements, then they'd be executed in reverse order but what I'd not given any thought to before was what happens if the defer
statement isn't reached. In fact, I'd just assumed it was more of a declaration that this code should always be executed on function exit and as I put mine right at the start of the function, this was effectively the case.
However, for some functions (probably most), you don't want this. You only want the deferred code executing if some else as happened. This is shown simply in The Swift Programming Language book example:
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
In this, if the file is not opened, then the deferred code should be executed. Another very important usage is:
extension NSLock
{
func synchronized<T>(@noescape closure: () throws -> T) rethrows -> T
{
self.lock()
defer
{
self.unlock()
}
return try closure()
}
}
If the lock is never obtained, then it should never be unlocked. In this case, this shouldn't have as the self.lock()
will not return until it obtains the lock but if that line were replaced with self.
This is how defer
works. If the defer
statement is never reached and/or encountered, then the deferred code block will never be executed. This includes branches (if
-statements etc.). The following example:
enum WhenToReturn
{
case After0
case After1
case After2
}
func deferTest(whenToReturn: WhenToReturn, shouldBranch: Bool)
{
print("Defer Test - whenToReturn:\(whenToReturn), shouldBranch:\(shouldBranch)")
defer
{
print("defer 0")
}
print("0")
if whenToReturn == WhenToReturn.After0
{
return
}
defer
{
print("defer 1")
}
print("1")
if whenToReturn == WhenToReturn.After1
{
return
}
if shouldBranch
{
defer
{
print("shouldBranch")
}
}
defer
{
print("defer 2")
}
print("3")
}
deferTest(WhenToReturn.After0, shouldBranch: false)
deferTest(WhenToReturn.After1, shouldBranch: true)
deferTest(WhenToReturn.After2, shouldBranch: false)
deferTest(WhenToReturn.After2, shouldBranch: true)
Results:
Defer Test - whenToReturn:After0, shouldBranch:false
0
defer 0
Defer Test - whenToReturn:After1, shouldBranch:true
0
1
defer 1
defer 0
Defer Test - whenToReturn:After2, shouldBranch:false
0
1
3
defer 2
defer 1
defer 0
Defer Test - whenToReturn:After2, shouldBranch:true
0
1
shouldBranch
3
defer 2
defer 1
defer 0
Program ended with exit code: 0
This shows that returning before and/or not branching results in defer
statements not being encountered hence the deferred code is not executed. This is no different to say a finally
-block
in C#. The reason for my initial confusion is that there is no additional content for a defer
block as there is for a finally
block, i.e., the presence of the try
, e.g.
try
{
// Try some stuff
}
finally
{
// Always do something having tried something regardless of whether it worked or not
}
Whereas the only and actual context of the defer
block is its position.