Let’s see what they return given the same initial input:And here’s what’s printed out:You probably saw it coming.
It appears that wrapping a statement inside a defer statement causes it to be, well, deferred past the moment the function returns a value.
The output proves this idea but there are a couple of inconsistencies.
First of all, it contradicts the definition, which states:A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.
But more importantly, it’s simply technically impossible to perform any task after the function has returned.
See, any function in practically any programming language ultimately finishes with a return statement.
Once it hits return the execution leaves the current scope, disposes all the local resources, pops the function from the stack and jumps to where it left off one step up the call hierarchy.
You might get confused into thinking that it‘s doable because you remember using autorelease in Objective-C which allowed for freeing up resources after returning them in a method.
Unfortunately, that’s a whole different story.
The way it works is that there’s an autorelease pool which drains at every iteration of the current Run Loop which sends a release message to each object marked as autoreleased within the current scope.
But defer doesn’t have these tricks up its sleeves.
To find out what’s actually going on let’s put it through Hopper Disassembler and check the results.
Compile the code you’ve written with the following command:xcrun swiftc your_source_code.
swift -o output_fileThen, download Hopper, run it and load output_file in there.
Hopper will break down the whole app into the list of executable processor instructions.
Also, for the sake of readability, it will generate a C-like pseudocode out of it.
That’s exactly what we need.
Here’s what a disassembled pseudocode of function b() looks like:It takes a while to process what’s going on here, so take your time.
First things first, the first function up there represents b(), and the second one is the closure that you pass to the defer operator.
Notice that return statements are, as expected, the last operations of both methods.
Calls to swift_beginAccess and swift_endAccess are the result of accessing the global variable a.
You don’t need to focus on all of the random variables being passed around in there, it’s not that relevant.
The key lines are 3, 6, and 7.
At line 3 the initial value of a is stored in rcx register.
Then, whatever you pass to defer is called at line 6.
In our case, defer generates a new string “ world” and concatenates it with the string a.
Ignore the fact that defer puts something into rax and returns it.
It’s just a convention that the returned value is stored in this register.
Now moving back to the first function.
As soon as we’re done with defer the previously saved value is moved back from rcx to rax at line 7 and is returned at the next step.
Long story short, the trick was to save the original value before calling defer and applying the side effect.
Therefore, the real equivalent of function b() without defer would involve a temporary variable to store the contents of a before modifying it:Not much of a difference.
However, it seemingly creates a bit of complexity by introducing one extra variable, so you might be wondering if it also creates any memory overhead.
Does it?.To dig deeper into this issue we first need to address the topic of how variables behave at assignment operation.
There are two types of data in Swift: those that are passed by value (structs and primitives) and those that are passed by reference (classes).
If you try to pull this off with a reference type object then it just won’t work the same way.
No matter how you do it, if you modify it inside the function it will return the latest edit.
But String is a value type, meaning that each assignment will spawn a new copy of it.
But of course, it’s not that simple.
String itself is just a wrapper of constant size of 16 bytes no matter how huge your wall of text is.
The underlying character buffer which contains the actual text, however, is a reference type, and it retains the copy-on-write semantics to save memory footprint during assigning operations.
Check out this WWDC video to learn more about copy-on-write.
So, regardless of how many String variables you declare that read the value from a, the actual copying only happens once when a.
append(“ world”) is called.
So in theory there is no significant burden caused by an extra variable, thanks to the copy-on-write semantics that String implements.
However, if you copy/paste the contents of disassembled defer into disassembled b() you’ll notice that the internals of b() and c() are nearly identical:Disassembled function b(), simplified pseudocodeDisassembled function `c()`, pseudocodeExample #2Moving on, let’s examine another fragment of code.
This one is more interesting.
In a way, it resembles a common use case of defer which is disposing a captured resource before leaving the current scope.
In our example it’s simplified as a global Optional <String> which is assigned a value at the beginning of a function and then turns into nil at the end of it.
This function will successfully print out “Hello world”.
It sure does make sense, but hold on.
Didn’t we just prove that a = nil inevitably occurs inside the function?.So then, how come it doesn’t crash at force unwrapping?Without further ado, let’s take a look at what this function looks like when put through Hopper:Whoa, a simple three-liner has turned into this abomination that spans 34 lines of code.
Don’t bother figuring out every single line in this pile of characters and digits, we’ll just need to group it into meaningful chunks to analyze the algorithm.
Lines 2 to 9, for example, represent the first line in the original function b() where we assign “Hello world” string to variable a.
The call to defer is that mere line number 20, which means that everything else in here belongs to the seemingly trivial operation return a!.
In fact, return a!.isn’t as simple as it looks.
Behind the scenes there are three operations going on:Reading the value of a and saving it in the local scope.
This happens between the second pair of swift_beginAccess/swift_endAccess at lines 10 and 16.
Unwrapping the value we received on the previous step.
This, as you’ve probably guessed already, is what the if statement between the lines 17 and 32 is for.
Lines 18 to 21 signify a successful outcome when the value exists, and lines 24 to 31 engage the crash routine which happens when you pass a nil for unwrapping.
Finally, the actual return!.It’s at line 33, you can’t miss it.
In case that was tough to process, I made a color-coded version:And here’s what these lines represent in the original function:Paying close attention to where defer can be found in the disassembled code answers many questions.
For example, now it’s clear that the app doesn’t crash because the assignment a = nil (red) happens after the last time the value of a has been read (blue).
But meanwhile there’s something more fascinating.
See how defer appears not before, not after, but in the middle of the force unwrapping routine.
It’s quite fascinating that you can find the deferred code injected into the implementation of another method.
One can argue though that the compiler doesn’t have to perform any complex calculations.
All it does it put the call to defer right before the function finishes.
And yet, the fact that it can traverse so deep is impressive.
The Conclusions!For such a simple operator, defer is doing a really good job of pushing itself to its limits.
The true power of this method is that it can “split” the conventional Swift return statement into individual low level operations and squeeze itself in between right before the very last returning processor instruction (aka ret).
This way, it makes for more elegant and natural code without the necessity of adding “temporary” intermediate variables.