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
~Copyablecasually. 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/borrowingon copyable types to micro-optimise without measuring. Profile first. - Using
discard selfwithout understanding it skipsdeinit— only valid inconsumingmethods of types with adeinitthat 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:
inoutis exclusive access, not transfer of ownership.
Exercises¶
- Write a noncopyable
TemporaryFilethat creates a file oninitand deletes it indeinit. Prove that moving it doesn't delete early. - Implement
consuming func finish() -> Receipton aTransaction: ~Copyableand show the compiler rejecting a second use. - Write functions taking an
Int-array-wrapping noncopyable struct byborrowing,inout, andconsuming; call each and note which calls invalidate the original. - Make a
Lock: ~CopyablewithwithLock { }that has adeinitverifying it isn't left locked. - Reproduce an overlapping-access error with
inout, then fix it. - If your toolchain has
Span, rewrite a function that takesUnsafeBufferPointer<UInt8>to takeSpan<UInt8>.
Recap¶
~Copyabletypes have unique ownership and may havedeinit; assignment moves them.- Parameters declare
borrowing,consuming, orinout;consumeends a lifetime early. - Noncopyable generics suppress the implicit
Copyablerequirement. Spanprovides safe, fast views over contiguous memory.- Exclusive access is always enforced: no overlapping modification.