How do noncopyable types behave in practice: deinit on a struct, consuming and borrowing methods, discard, and what they cannot do yet?
Predict first
When does each close line print?
struct Handle: ~Copyable {
let fd: Int
init(_ fd: Int) { self.fd = fd; print("open \(fd)") }
deinit { print("close \(fd)") }
borrowing func peek() { print("peek \(fd)") }
consuming func finish() { print("finish \(fd)") } // self ends here, so deinit runs
consuming func detach() -> Int { let f = fd; discard self; return f } // skips deinit
}
func spend(_ h: consuming Handle) { print("spending \(h.fd)") }
func demo() {
let a = Handle(1)
a.peek()
a.finish()
print("after finish")
let b = Handle(2)
print("detached", b.detach())
let c = Handle(3)
spend(c)
print("after spend")
let d = Handle(4)
print("end of demo, d is", d.fd)
}
demo()
- open 1, peek 1, finish 1, close 1, after finish, open 2, detached 2, open 3, spending 3, close 3, after spend, open 4, end of demo, d is 4, close 4
- close lines all print at the end of demo
- finish 1 and spending 3 print, but close never runs for moved values
Mental model. A noncopyable value is a physical key rather than a photo of one: handing it over means you no longer have it, and the lock-up routine (deinit) runs when the last holder is done.
A ~Copyable struct or enum has unique ownership enforced by the compiler: assigning or passing it moves it, and using it afterwards is an error. That makes it a natural wrapper for a file descriptor, a lock or a one-shot token, and it can have a deinit, which classes had alone until now. The check shows the rules: a borrowing method only reads, so the value lives on; a consuming method takes ownership, so deinit runs when that method ends (close 1 comes before after finish); a consuming parameter does the same for a function (close 3 before after spend); a value never consumed is destroyed at the end of its scope (close 4 last). A consuming method that wants to take the fields apart without running deinit says discard self (detach printed no close).
The limits, which depend on the Swift version: a plain function parameter must say borrowing or consuming (the compiler requires an ownership choice), a noncopyable value cannot be captured by an escaping closure or stored in a plain Array (Array still requires Copyable elements), and a struct that contains one is itself noncopyable. Swift 6 lets Optional and generics opt out of Copyable (Optional<Token>.take() works, and Mutex in the Synchronization module is a noncopyable type). Use them where a resource must have exactly one owner and a precise end of life; use a final class when you need sharing or reference identity.
Consuming ends the value inside the callee, discard self skips deinit, and an untouched value dies at scope end.
Common traps
- Expecting a noncopyable struct’s
deinitto run at the closing brace after a consuming call. It runs when the value is consumed, inside the callee. - Putting a noncopyable value in an array or a closure and expecting it to compile. Those APIs still need copyable values (check the error for your toolchain).
They might follow up with
- How is this different from
consumeon a normal value? For copyable valuesconsumeonly ends the variable’s use; see the next question. - When would you reach for it? A resource wrapper where double-close or use-after-close must be a compile error.
Related questions
- 3.8.5 What does the consume operator do for ordinary values, and does it release a class instance earlier?
- 3.6.2 What do guaranteed and owned parameters mean, and what do borrowing and consuming add?
- 1.8.5 What are consume, borrowing, and noncopyable types, at a high level?
- 3.6.1 Where do hidden retains and releases come from, and how do you reduce them?
← Which thread does deinit run on, and what does isolated deinit change… What does the consume operator do for ordinary values, and does it… →