Skip to content

8. Ownership: noncopyable types, borrowing & consuming

What & why

Normally Swift copies value types freely and ARC manages classes, so you rarely think about who owns what. But some values shouldn't be copied: a file descriptor, a lock, a unique token, a one-shot continuation. And in performance-critical code, copies and reference-count traffic cost real time. Swift's ownership features (Swift 5.9 onward, expanded in 6.0 and 6.2) let you state ownership precisely and have the compiler enforce it.

Noncopyable types: ~Copyable

Add ~Copyable ("not copyable") to a struct or enum:

struct FileDescriptor: ~Copyable {
    private let fd: Int32

    init(fd: Int32) { self.fd = fd }

    func describe() -> String { "fd \(fd)" }

    consuming func close() {
        print("closing \(fd)")
        // c-library close(fd) would go here
        discard self                      // skip deinit: we've already cleaned up
    }

    deinit {
        print("deinit closes \(fd)")
        // close(fd)
    }
}

A noncopyable type has unique ownership, like a std::unique_ptr or Rust's non-Copy types. It can have a deinit (structs normally can't), which runs when the single owner goes away — deterministic RAII for value types.

func demo() {
    let a = FileDescriptor(fd: 3)
    let b = a                          // MOVE, not copy: `a` is no longer usable
    // print(a.describe())             // ERROR: 'a' used after consume
    print(b.describe())
}                                       // b destroyed here → "deinit closes 3"

Assignment, passing as an argument (by default), and returning all move the value.

Borrowing and consuming parameters

Parameters of noncopyable types need an ownership convention:

Keyword Meaning Caller afterwards
borrowing read-only access; callee can't keep it still owns it
inout exclusive read/write access still owns it
consuming callee takes ownership (and may destroy or store it) can't use it any more
func inspect(_ file: borrowing FileDescriptor) {
    print(file.describe())            // can read; cannot store or consume it
}

func finish(_ file: consuming FileDescriptor) {
    file.close()                       // consumes
}

let f = FileDescriptor(fd: 7)
inspect(f)                             // fine; f still ours
inspect(f)                             // fine again
finish(f)                              // ownership transferred; f is dead after this
// inspect(f)                          // ERROR: 'f' consumed

For copyable types the same keywords are optimisation hints and semantics controls:

func lengthOf(_ s: borrowing String) -> Int { s.count }       // avoids a retain/release
func storing(_ s: consuming String) -> [String] { [s] }       // caller's copy can be forwarded, not duplicated

Defaults for copyable types: normal parameters are borrowing-like (the callee gets a +0 reference), init parameters are consuming, and self in methods is borrowed unless marked consuming or mutating.

consume and copy operators

var message = "hello"
let moved = consume message           // `message` can't be used again (until reassigned)
// print(message)                      // ERROR
message = "reborn"                     // fine: reinitialised

let c = copy message                   // explicit copy of a copyable value

consume is useful to end a variable's lifetime early (releasing resources sooner) or to avoid a copy when handing over a large value.

Noncopyable generics and protocols

Generics are Copyable by default; suppress it to accept noncopyable types:

struct Box<T: ~Copyable>: ~Copyable {
    private var value: T
    init(_ value: consuming T) { self.value = value }
    consuming func unbox() -> T { value }
}

let box = Box(FileDescriptor(fd: 9))
let file = box.unbox()

The standard library adopts this progressively: Optional supports noncopyable wrapped types (so FileDescriptor? works), Result, and some others. Span and friends below are built on it.

Span: safe views of contiguous memory (Swift 6.2)

UnsafeBufferPointer is fast but unsafe. Span<Element> (and RawSpan, MutableSpan) is a non-escaping view of contiguous memory with bounds checking and compiler-enforced lifetimes — it can't outlive the storage it views, with no ARC traffic.

func sum(_ numbers: Span<Int>) -> Int {
    var total = 0
    for i in numbers.indices { total += numbers[i] }
    return total
}

let array = [1, 2, 3, 4]
print(sum(array.span))                // 10 — borrowing the array's storage

Spans are ~Escapable: values whose lifetime is bound to something else. That is how Swift gets the speed of pointers with the safety of references. Check your toolchain version: Span shipped with Swift 6.2.

Where noncopyable types shine

  • Resources with a lifetime: file handles, sockets, database transactions, GPU buffers, locks (Mutex's guard).
  • Single-use tokens: a "permission to send once" value that is consumed when used.
  • Compile-time state machines: consuming func authenticate() -> AuthenticatedSession, so an unauthenticated session can't be used after authentication.
  • Performance-critical containers that avoid reference counting.
  • Embedded/systems Swift, where no heap and no hidden copies is the goal.
struct Draft: ~Copyable {
    var text: String
    consuming func publish() -> Published { Published(text: text) }
}
struct Published { let text: String }

let draft = Draft(text: "Hello")
let post = draft.publish()
// draft.text                          // ERROR: consumed — you can't edit a published draft by accident

Exclusive access (the law of exclusivity)

Related and always on: Swift guarantees that while you're modifying a variable, nothing else can access it.

var numbers = [1, 2, 3]
// numbers.append(numbers.count)        // fine: the argument is evaluated first
func modify(_ a: inout [Int], _ b: inout [Int]) {}
// modify(&numbers, &numbers)           // ERROR: overlapping accesses to 'numbers'

Most violations are caught at compile time; dynamic ones (class properties, globals) trap at runtime with "Simultaneous accesses". This is why swap(&a[i], &a[j]) doesn't compile and you use a.swapAt(i, j) instead.

Common mistakes

  • Reaching for ~Copyable casually. It makes APIs more restrictive. Use it when uniqueness is the point.
  • Forgetting that moves are implicit — passing a noncopyable value to a function consumes it unless the parameter is borrowing.
  • Using consuming/borrowing on copyable types to micro-optimise without measuring. Profile first.
  • Using discard self without understanding it skips deinit — only valid in consuming methods of types with a deinit that must not run twice.
  • Expecting noncopyable support everywhere: closures, some generic APIs, and many library types still require Copyable. The surface grows each release.
  • Confusing ownership with mutation: inout is exclusive access, not transfer of ownership.

Exercises

  1. Write a noncopyable TemporaryFile that creates a file on init and deletes it in deinit. Prove that moving it doesn't delete early.
  2. Implement consuming func finish() -> Receipt on a Transaction: ~Copyable and show the compiler rejecting a second use.
  3. Write functions taking an Int-array-wrapping noncopyable struct by borrowing, inout, and consuming; call each and note which calls invalidate the original.
  4. Make a Lock: ~Copyable with withLock { } that has a deinit verifying it isn't left locked.
  5. Reproduce an overlapping-access error with inout, then fix it.
  6. If your toolchain has Span, rewrite a function that takes UnsafeBufferPointer<UInt8> to take Span<UInt8>.

Recap

  • ~Copyable types have unique ownership and may have deinit; assignment moves them.
  • Parameters declare borrowing, consuming, or inout; consume ends a lifetime early.
  • Noncopyable generics suppress the implicit Copyable requirement.
  • Span provides safe, fast views over contiguous memory.
  • Exclusive access is always enforced: no overlapping modification.

Next: Performance & profiling →