Defer with return of local mutable variable

I have a simple code with surprising behavior:

func testReturn(val: String) -> Int {
  var variable: Int = -1
  defer {
    variable = 2
  }
  
  guard !val.isEmpty else {
    return variable
  }
  return variable // 💥 always returns -1
}

testReturn2(val: "a") // -1
testReturn2(val: "") // -1

It is written in documentation: A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.
So I expect that before returning defer block is executed and function returns 2.

There is also a message of Joe Goff in similar topic dated to 2016:

Should defer update local variable and return this updated value or there are some details I'm missing?

I would say that this is correct behavior. The returned expression evaluates and produces the return value before defers are run, giving you the value of variable at that point.

6 Likes

So, just for clarifying, the order is:

  1. execute function body
  2. prepare return value
  3. execute defer body
  4. return of prepared earlier return value
    ?

Yeah, that's right.

I've observed that returning the value of a mutable variable, and then mutating it, is the most common use case of defer. (I.e. I'm surprised that you were surprised!) Everything else that I've seen people use defer for can be done without defer, by just moving the same code later into the function body.

defer still isn't ever necessary, but it does eliminate the need for constants that only exist to capture the value of a variable. E.g.

public extension AdditiveArithmetic where Self: ExpressibleByIntegerLiteral {
  static func fibonacciSequence(_ numbers: (Self, Self) = (0, 1)) -> some Sequence<Self> {
    sequence(state: numbers) { numbers in
      defer { numbers = (numbers.1, numbers.0 + numbers.1) }
      return numbers.0
    }
  }
}
1 Like

This is an excellent article about how defer actually works, and an explanation on why your variable wasn't mutated before return by the defer

1 Like

Isn't the primary purpose to protect against resource leaks in case of error being thrown? e.g.

func readSomethingFromResourceAndThenCloseIt(name: String) throws -> String 
{
    var handle = try openResource(name)
    defer { handle.close() }
    return try handle.readAThing()
}

It can be done without defer but then you'd have to catch errors and rethrow them. It would be nastier than implementing your example without defer.

Edit the above example shows another reason why you need defer to run after evaluating the return expression. If it ran before, the call to readAThing() above would happen on a closed resource.

2 Likes