Noncopyable types and ownership

Swift · Expert · 16 min

Noncopyable types give compile-time guarantees for files, locks and tokens that must exist exactly once.

Normally every Swift type is implicitly Copyable. Since Swift 5.9 you can opt out with `~Copyable`: struct FileHandle: ~Copyable. Values of such a type have a single owner; assignment and passing move the value instead of copying it, and the compiler rejects any use of the old variable afterwards.

Noncopyable structs and enums can have a deinit, which runs exactly once when the owner's lifetime ends — ideal for closing a file descriptor or releasing a lock without a class or ARC.

Parameters of noncopyable type need an explicit ownership convention:

  • borrowing — the function gets read access temporarily; the caller keeps ownership.
  • consuming — the function takes ownership; the caller can't use the value afterwards.
  • inout — temporary exclusive mutable access.

The same words apply to methods: a consuming func close() ends the value's life, and inside it discard self skips the deinit when cleanup was already done manually.

The `consume` operator ends a variable's lifetime explicitly, even for copyable values: let b = consume a lets the optimizer avoid a copy and makes later use of a an error.

Generics can accept noncopyable types with <T: ~Copyable>, and since Swift 6 Optional and Result support them. Newer library types such as Mutex, Atomic, Span and InlineArray build on these features.

Example

struct TempFile: ~Copyable {
    let fd: Int32
    init(fd: Int32) { self.fd = fd; print("open", fd) }
    borrowing func write(_ s: String) { print("write \(s) to", fd) }

    consuming func persist() -> Int32 {
        let kept = fd
        discard self                  // skip deinit: file is kept
        return kept
    }
    deinit { print("delete", fd) }
}

func log(_ f: borrowing TempFile) { f.write("log") }

do {
    let a = TempFile(fd: 3)           // open 3
    log(a)                            // write log to 3
    let b = a                         // move, not copy
    // a.write("x")                   // error: 'a' used after consume
    _ = b
}                                     // delete 3

let kept = TempFile(fd: 4).persist()
print("kept", kept)                   // open 4, kept 4 (no delete)

Remember

  • ~Copyable types have one owner; assignment moves them
  • Parameters use borrowing, consuming or inout
  • Noncopyable structs can have a deinit; discard self skips it

Watch out for

  • Forgetting the ownership modifier on a noncopyable parameter, which the compiler requires.

Check yourself

  1. After `let b = a` where `a` is a ~Copyable struct, what happens when you call a method on `a`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/

Builds on

Unlocks

Nothing builds on this yet.