Defer return
return and defer meet at the end of a function, and the order between them is fixed. return value; works out its value first and keeps it; only then do the deferred statements run. Whatever they change afterwards, the caller still receives the kept value.
That sounds like a detail, but it makes a useful pattern possible: "hand out the current value, then move on" becomes a two-line function, with no temporary variable to hold the old value.
The order at the end of a function
Countdown registers two deferred statements and then returns its local:
func Countdown() -> int32 {
var remaining: int32 = 3;
defer PrintLine(" deferred code sees remaining = {}", remaining);
defer remaining = 0;
return remaining;
}
Step by step, as the function ends:
flowchart LR
r["`return remaining`<br/>works out `3`<br/>and keeps it"] --> d1["`defer remaining = 0`<br/>(registered last, runs first)"]
d1 --> d2["`defer PrintLine(…)`<br/>prints `remaining = 0`"]
d2 --> c["the caller<br/>receives `3`"]countdown:
deferred code sees remaining = 0
returned 3
Two things are worth noticing in that output.
The PrintLine shows 0, not 3. A deferred statement is evaluated when it runs, not when it is registered — so it reads remaining after the reset has happened. And the reset did happen: the local really is 0. It simply makes no difference to the caller, who gets the 3 that return had already kept.
Hand out, then advance
A ticket dispenser shows a number. Taking a ticket gives you that number, and the display moves on to the next:
struct Dispenser {
next: int32;
}
extend Dispenser {
// Returns the current ticket, then advances the dispenser.
func Take(self: &var Dispenser) -> int32 {
defer self.next += 1;
return self.next;
}
}
return self.next keeps the current number; the deferred self.next += 1 then advances the dispenser. Without defer, the same method needs a temporary:
let ticket = self.next;
self.next += 1;
return ticket;
Both are correct. The defer version says what the method is for in its last line — return the current ticket — and puts the bookkeeping on the line before it.
var dispenser = Dispenser { next: 1 };
let first = dispenser.Take();
let second = dispenser.Take();
first is 1, second is 2, and the dispenser now shows 3. The method changes self, so it takes &var Dispenser, as in Mutating method.
| Statement | When it is evaluated | What the caller sees |
|---|---|---|
return self.next; | first — its value is kept | the kept value |
defer self.next += 1; | after the return value is kept | the change, on its next call |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// `return` and `defer` meet at the end of a function, and the order between them is fixed:
// `return value;` works out its value first and keeps it, and only then do the deferred
// statements run. Whatever they change afterwards, the caller still receives the kept value.
//
// That makes "hand out the current value, then move on" a two-line function. A ticket dispenser
// returns the number it is showing and advances to the next one, without a temporary variable to
// hold the old number.
import Io::PrintLine;
// Both deferred statements run after the return value is kept. In reverse order, so the reset
// runs first and the `PrintLine` then shows that the local really changed. The caller gets 3.
func Countdown() -> int32 {
var remaining: int32 = 3;
defer PrintLine(" deferred code sees remaining = {}", remaining);
defer remaining = 0;
return remaining;
}
struct Dispenser {
next: int32;
}
extend Dispenser {
// Returns the current ticket, then advances the dispenser.
func Take(self: &var Dispenser) -> int32 {
defer self.next += 1;
return self.next;
}
}
func Main() -> int {
PrintLine("countdown:");
let result = Countdown();
PrintLine(" returned {}", result);
PrintLine("tickets:");
var dispenser = Dispenser { next: 1 };
let first = dispenser.Take();
let second = dispenser.Take();
PrintLine(" took {} and {}, now showing {}", first, second, dispenser.next);
return 0;
}
Run it
cd Examples/Ownership/DeferReturn
rux run
countdown:
deferred code sees remaining = 0
returned 3
tickets:
took 1 and 2, now showing 3
Common mistakes
Try it yourself
- Swap the two
deferlines inCountdown. What does the deferredPrintLineshow now, and does the returned value change? - Add a method
Peek(self: &Dispenser) -> int32that returns the next ticket without taking it, and print it between the twoTakecalls. - Write
func Next(counter: &var int32) -> int32that returns the counter's current value and then adds 1 to it, usingdefer. Call it twice on avarthat starts at 5.
Learn more
- Defer — registering cleanup, and the reverse order
- Mutating method —
self: &var T - Return — returning a value from a function