How do noncopyable types behave in practice: deinit on a struct, consuming and borrowing methods, discard, and what they cannot do yet?

3.8.4 · L5 · Interview

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 deinit to 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 consume on a normal value? For copyable values consume only 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… →