6

Im testing Swift 2.0 and the new keyword defer in a Playground:

func branch() -> String {

    var str = ""

    defer { str += "xxx" }
    str += "1"

    let counter = 3;

    if counter > 0 {
        str += "2"
        defer { str += "yyy" }
        str += "3"
    }      
    str += "4"

    return str    
}

let bran = branch()

I expected bran to be "123yyy4xxx", but it actually is "123yyy4"

Why did my defer (str += "xxx") not work as expected?

luk2302
  • 55,258
  • 23
  • 97
  • 137
Vic
  • 758
  • 2
  • 15
  • 31
  • 1
    it works but the `defer` is invoked _after_ the scope runs out, therefore first the `return str` is called then the scope runs out and it calls your `defer` and add the `"xxx"` to the local instance which happens _after_ the value returns. – holex Jan 20 '16 at 09:22

3 Answers3

7

A defer statement defers execution until the current scope is exited.

That what apple says. So the defer statement will be executed after return statement. That's why you cannot see the expected result.

Greg
  • 25,317
  • 6
  • 53
  • 62
7

Greg is correct and if you want to get same result with your code then you can do it this way:

var str = ""

func branch() {

    str = ""
    defer { str += "xxx" }
    str += "1"


    let counter = 3

    if counter > 0 {
        str += "2"
        defer { str += "yyy" }
        str += "3"
    }
    str += "4"

}

branch()
str    //"123yyy4xxx"
Dharmesh Kheni
  • 71,228
  • 33
  • 160
  • 165
  • 2
    Global variables based solutions are almost never good solutions. You also forgot to reset the `str` variable, so calling `branch()` multiple times gives different results. – Cristik Jan 20 '16 at 09:34
5

Firstly: the defer gets executed as you can clearly see when adding a print(str) to it.

Now to explain why the returned value does not reflect the changed value:
The reason for this is that String is immutable - whenever you write str += something you create an entirely new String instance and store it inside str.

If you write return str that returns the current instance of str which is 123yyy4. Then the defer gets invoked and assigns the completely new and unrelated String 123yyy4xxx to str. But that does not change the previous String object stored inside str, it simply overwrites it and therefore does not affect the return which has already "happened".

If you change your method to use NSMutableString instead you will always operate on the same instance and the result will therefore correctly output 123yyy4xxx:

func branch() -> NSMutableString {
    var str = NSMutableString()
    defer { str.appendString("xxx") }
    str.appendString("1")
    let counter = 3;
    if counter > 0 {
        str.appendString("2")
        defer { str.appendString("yyy") }
        str.appendString("3")
    }
    str.appendString("4")
    return str
}


let bran1 = branch()

In that code the return returns the instance stored in str and the defer alters that instance, it does not assign a new instance but changes the already present one.

For explanation sake you can try to view the memory address of str at different stages:

  • at the time of the return
  • before changing str in the defer block
  • after changing it

For the NSMutableString all three cases will yield the same memory address meaning that the instance stays the same. The String one however prints two different memory addresses resulting in the returned String to point to someAddress and the deferred one to point to someOtherAddress.

luk2302
  • 55,258
  • 23
  • 97
  • 137
  • 3
    Note that `unsafeAddressOf()` is completely meaningless with a Swift string (or any non-class type), it returns the address of a temporarily bridged NSString: http://stackoverflow.com/questions/32638879/swift-strings-and-memory-addresses. – Martin R Jan 20 '16 at 09:08
  • @MartinR hmm :/ any Idea what I should use instead to visualize the memory change of the String instance? – luk2302 Jan 20 '16 at 09:10