Copyable vs Noncopyable in Swift — A Friendly, Step‑by‑Step Guide

From Qiki
Jump to navigation Jump to search

Copyable vs Noncopyable in Swift — A Friendly, Step‑by‑Step Guide

Table of Contents

  1. Big Picture (Why this matters)
  2. What “copying” means in Swift
    1. 2.1 Value types (struct/enum)
    2. 2.2 Reference types (class)
    3. 2.3 Implicit copies vs explicit copies
  3. Copyable and ~Copyable (noncopyable)
    1. 3.1 The idea
    2. 3.2 Declaring a noncopyable type
    3. 3.3 Lifetime and deinit for noncopyable value types
  4. Ownership when calling functions
    1. 4.1 borrowing
    2. 4.2 consuming
    3. 4.3 inout
    4. 4.4 The copy and consume operators
    5. 4.5 Quick comparison table
  5. Beginner examples (Copyable types)
  6. Beginner → Intermediate (Declaring noncopyable)
  7. Intermediate (borrowing vs inout vs consuming)
  8. Intermediate → Advanced (methods, deinit, and enums)
  9. Generics, extensions, and conditional conformances
  10. Common pitfalls (and what the compiler says)
  11. Practice problems – Set A (Beginner) – Set B (Intermediate) – Set C (Advanced)
  12. Answer key (with fixes and explanations)



1) Big Picture (Why this matters)

Swift used to quietly copy your values whenever needed. That’s easy, but sometimes copying is wrong (for example: a “single‑use ticket” should not be duplicated) or too expensive (large structs). Newer Swift lets you be clear about ownership: you can say “this function just borrows your value”, “this function takes it and you can’t use it again”, or “this function mutates it in place”.

Goal of this guide: Build a simple mental model and then practice with real code.

Mental model: Copyable values are like photocopies of a paper. Noncopyable values are like your real passport — you can pass it around to check it (borrow), give it to someone (consume), or update it (inout), but you can’t clone it.


2) What “copying” means in Swift

2.1 Value types (struct/enum)

  • Assigning or passing a struct/enum normally copies the bits.
  • Example: var a = Point(x: 1, y: 2); var b = ab is a separate value.

2.2 Reference types (class)

  • Assigning or passing a class copies the reference, not the object.
  • Example: var a = Box(10); var b = aa and b point to the same object.

2.3 Implicit copies vs explicit copies

  • By default, Swift may copy implicitly to make your code work.
  • With the new ownership features, you can turn off implicit copies inside certain functions and require copy x to be explicit when needed.



3) Copyable and ~Copyable (noncopyable)

3.1 The idea

  • Copyable is a marker that says a type can be copied. Most types are Copyable by default.
  • ~Copyable means “not copyable”. You opt out of copying.

3.2 Declaring a noncopyable type

struct SingleUseTicket: ~Copyable {
    let id: Int
}
  • You can move or borrow a SingleUseTicket, but you can’t duplicate it.

3.3 Lifetime and deinit for noncopyable value types

Because a noncopyable value has a unique identity, structs and enums marked ~Copyable can have a deinit, which runs at the end of the value’s lifetime (similar to classes):

struct FileHandle: ~Copyable {
    let path: String
    var isOpen = true

    deinit {
        if isOpen { print("Auto‑closing \(path)") }
    }

    mutating func write(_ text: String) {
        precondition(isOpen)
        print("→ write to", path, ":", text)
    }
}

Tip: You can also write consuming methods on noncopyable types (see §8). A consuming method takes ownership of self and ends its lifetime by the end of the call.


4) Ownership when calling functions

There are three main ways to pass a value:

4.1 borrowing

  • The function borrows a read‑only view temporarily.
  • Caller keeps ownership. Callee can’t keep or consume it.
func inspect(_ t: borrowing SingleUseTicket) {
    print("Ticket #", t.id)
    // t can’t be consumed or stored beyond this call
}

4.2 consuming

  • The function takes ownership. After the call, the caller can’t use the value anymore.
func use(_ t: consuming SingleUseTicket) {
    // t is now owned here; caller loses it
    print("Using ticket #", t.id)
    // t’s lifetime ends by the end of this function
}

var ticket = SingleUseTicket(id: 42)
use(ticket)
// ❌ error if we try: print(ticket)  // “'ticket' used after consume”

4.3 inout

  • The function gets exclusive, mutable access to the caller’s value. It must return it to the caller.
func punch(_ t: inout SingleUseTicket) {
    print("Punching #", t.id)
    // can mutate fields if they’re var
}

var t2 = SingleUseTicket(id: 7)
punch(&t2)   // caller still owns t2 afterwards

4.4 The copy and consume operators

  • copy x: make an explicit copy (only for Copyable values). Often required inside borrowing/consuming functions when you need a duplicate.
  • consume x: move the current value out of a local/parameter, ending that binding’s lifetime.
func duplicate(_ s: borrowing String) -> (String, String) {
    let a = copy s
    let b = copy s
    return (a, b)
}

var fh = FileHandle(path: "/tmp/log.txt")
let owned = consume fh
// fh is now invalid here; we moved it into `owned`

4.5 Quick comparison table

How you pass Callee can read? Callee can mutate? Callee can keep/consume? Caller can still use after?
borrowing
inout ✅ (with mutations)
consuming ✅ (owns it)



5) Beginner examples (Copyable types)

Example 5.1 – Struct copy vs class reference

struct Point { var x: Int; var y: Int }
class Box { var value: Int; init(_ v: Int) { value = v } }

var p1 = Point(x: 1, y: 2)
var p2 = p1            // copies bits
p2.x = 99
print(p1.x, p2.x)     // 1, 99

let b1 = Box(10)
let b2 = b1           // copies reference
b2.value = 99
print(b1.value, b2.value) // 99, 99 (same object)

Example 5.2 – borrowing with explicit copy

func shoutTwice(_ s: borrowing String) -> String {
    // `s` isn’t implicitly copyable here
    let twice = copy s + " " + copy s
    return twice.uppercased()
}
print(shoutTwice("hello"))  // HELLO HELLO

6) Beginner → Intermediate (Declaring noncopyable)

Example 6.1 – A single‑use ticket

struct SingleUseTicket: ~Copyable { let id: Int }

func scan(_ t: borrowing SingleUseTicket) {
    print("Scanning #", t.id)
}

func enter(_ t: consuming SingleUseTicket) {
    print("Welcome with #", t.id)
}

var t = SingleUseTicket(id: 101)
scan(t)      // OK (borrow)
enter(t)     // OK (consume)
// print(t)  // ❌ error: 't' used after consume

Example 6.2 – Noncopyable + deinit

struct TempDir: ~Copyable {
    let path: String
    deinit { print("Cleaning up", path) }
}

func makeAndDrop() {
    var d = TempDir(path: "/tmp/work")
    // end of scope → runs deinit automatically
}
makeAndDrop()

7) Intermediate (borrowing vs inout vs consuming)

struct Counter: ~Copyable {
    var value: Int
    mutating func inc() { value += 1 }
    consuming func take() -> Int { return value }
}

func read(_ c: borrowing Counter) {
    print("peek:", c.value)     // read only
}

func bump(_ c: inout Counter) {
    c.inc()                      // mutate in place
}

func drain(_ c: consuming Counter) -> Int {
    c.take()                     // take ownership and end lifetime
}

var c = Counter(value: 0)
read(c)             // borrow
bump(&c)            // inout, c now 1
let n = drain(c)    // consume, caller loses `c`
print(n)
// print(c.value)   // ❌ error: use after consume

Key differences:

  • borrowing → easy, safe read.
  • inout → exclusive mutation, must give value back.
  • consuming → moves ownership to the callee.



8) Intermediate → Advanced (methods, deinit, and enums)

8.1 Consuming methods on noncopyable types

struct Socket: ~Copyable {
    var isOpen = true
    mutating func send(_ s: String) { precondition(isOpen); print("→", s) }
    consuming func close() { print("closing"); /* end lifetime */ }
}

var s = Socket()
s.send("PING")
s.close()        // after this, `s` is gone

8.2 Pattern matching with noncopyable enums

enum Resource: ~Copyable {
    case open(Socket)
    case closed
}

func describe(_ r: consuming Resource) {
    switch consume r {              // consume for a full match
    case .open(let s): print("open")
    case .closed:       print("closed")
    }
}

Note: Newer toolchains improve “borrowing switches” too, but switch consume is a good mental model today.


9) Generics, extensions, and conditional conformances

9.1 Generic algorithms that work with copyable inputs

// Only works for types you can duplicate
func pair<T: Copyable>(_ x: borrowing T) -> (T, T) {
    (copy x, copy x)
}

9.2 Generic functions that can accept noncopyable inputs

// Works for any T as long as we only *borrow* it
func logType<T>(_ x: borrowing T) { /* read-only use of x */ }

If you need to consume a generic T, mark the parameter consuming T and make sure your algorithm doesn’t require copying. If you need duplication, constrain T: Copyable and use copy.

9.3 Extensions and conformances

You can extend your noncopyable types and add consuming or mutating methods. Protocol conformances for noncopyable types are available in modern Swift toolchains; check your compiler version if you hit a limitation.

protocol Closable { consuming func close() }

struct Ticket: ~Copyable { let id: Int }
extension Ticket: Closable { consuming func close() { /* ... */ } }

10) Common pitfalls (and what the compiler says)

  1. Trying to copy a noncopyable value
let t = SingleUseTicket(id: 1)
let t2 = t  // ❌ error: value of noncopyable type 'SingleUseTicket' cannot be copied

Fix: pass it as borrowing/inout, or move it using consume / consuming.

  1. Using a value after it’s been consumed
var t = SingleUseTicket(id: 2)
let moved = consume t
print(t)      // ❌ error: 't' used after consume

Fix: Only use moved afterwards.

  1. Forgetting ownership on function parameters
// Needs an ownership annotation for noncopyable
func bad(_ t: SingleUseTicket) { }
//    ^ may be rejected for noncopyable; use borrowing/consuming/inout

Fix: choose one: borrowing, inout, or consuming.

  1. Capturing a noncopyable local in a closure (advanced)
  • Once a local escapes into a closure, you generally can’t consume it later. Keep noncopyable lifetimes simple: consume them before they escape.
  1. Switching on a noncopyable enum without consume
func f(_ r: Resource) {  // if Resource is noncopyable
    // switch r   // ❌ often needs: switch consume r
}

Fix: switch consume r (or use a borrowing switch in newer toolchains).

  1. Trying to consume a global
  • Consuming noncopyable globals is limited. Wrap the operation in a function where the value is local and can be consumed safely.



11) Practice problems

Set A (Beginner)

A1) Write a function centered(_ p: borrowing Point) -> Point that returns a new Point at (p.x - 10, p.y - 10) using copy.

A2) Turn this into a noncopyable type:

struct Token { let id: Int }

Make a function redeem(_:) that consumes the token.

A3) Why does this fail?

struct Badge: ~Copyable { let id: Int }
let b = Badge(id: 1)
let c = b   // ???

Explain what the compiler is telling you in plain words.



Set B (Intermediate)

B1) Make Counter noncopyable and add:

mutating func add(_ n: Int)
consuming func take() -> Int

Write three functions: peek(_:) (borrowing), bump(_:) (inout), and drain(_:) (consuming) and show how calls affect the caller’s variable.

B2) Write a generic function duplicateIfCopyable:

func duplicateIfCopyable<T>(_: borrowing T) -> (T?, T?)
  • If T: Copyable, return (x, x) using copy.
  • Otherwise, return (nil, nil). (Hint: use an overload with T: Copyable.)

B3) For a noncopyable enum

enum Door: ~Copyable { case open; case closed }

write a function that prints the state using switch consume.



Set C (Advanced)

C1) Add a deinit to a noncopyable TempFile that prints when it cleans up. Show a timeline where deinit runs.

C2) Define a protocol:

protocol Closable { consuming func close() }

Make a noncopyable type Socket: ~Copyable conform and implement close().

C3) Show why this code is rejected and fix it:

func pair<T>(_ x: borrowing T) -> (T, T) { (x, x) }

(No copying is allowed on a borrowing parameter unless you say it.)



12) Answer key (with fixes and explanations)

Set A

A1)

struct Point { var x: Int; var y: Int }
func centered(_ p: borrowing Point) -> Point {
    var c = copy p
    c.x -= 10; c.y -= 10
    return c
}

Why: p is not implicitly copyable here, so we use copy p explicitly.

A2)

struct Token: ~Copyable { let id: Int }
func redeem(_ t: consuming Token) { print("Redeemed", t.id) }

Why: A noncopyable token can be consumed once to prevent reuse.

A3) The compiler says the value can’t be copied. Assigning b to c would duplicate a Badge, which is forbidden for ~Copyable types.



Set B

B1)

struct Counter: ~Copyable {
    var value: Int
    mutating func add(_ n: Int) { value += n }
    consuming func take() -> Int { value }
}

func peek(_ c: borrowing Counter) { print(c.value) }
func bump(_ c: inout Counter) { c.add(1) }
func drain(_ c: consuming Counter) -> Int { c.take() }

var c = Counter(value: 0)
peek(c)            // prints 0
bump(&c)           // c.value = 1
let v = drain(c)   // moves it; c is gone here
print(v)           // 1

B2)

// Fallback when T isn’t Copyable
func duplicateIfCopyable<T>(_ x: borrowing T) -> (T?, T?) { (nil, nil) }

// Overload for copyable types
func duplicateIfCopyable<T: Copyable>(_ x: borrowing T) -> (T?, T?) {
    let a = copy x
    let b = copy x
    return (a, b)
}

Why: The Copyable-constrained overload enables copy.

B3)

enum Door: ~Copyable { case open; case closed }
func printDoor(_ d: consuming Door) {
    switch consume d {
    case .open:   print("open")
    case .closed: print("closed")
    }
}

Set C

C1)

struct TempFile: ~Copyable {
    let path: String
    deinit { print("Removing", path) }
}

func demo() {
    var f = TempFile(path: "/tmp/demo.txt")
    // do work
} // leaving scope → prints "Removing /tmp/demo.txt"

C2)

protocol Closable { consuming func close() }
struct Socket: ~Copyable {
    var isOpen = true
    consuming func close() { if isOpen { print("closed"); } }
}
extension Socket: Closable {}

C3)

// Original (rejected):
// func pair<T>(_ x: borrowing T) -> (T, T) { (x, x) }

// Fix for copyable types:
func pair<T: Copyable>(_ x: borrowing T) -> (T, T) { (copy x, copy x) }

Why: A borrowing parameter isn’t implicitly copyable; use copy, and only if T is Copyable.



Final tips

  • Start with borrowing for read‑only helpers.
  • Use inout when a function needs to mutate and return the same value.
  • Use consuming to move ownership (single‑use, cleanup-on-drop types).
  • Reach for ~Copyable when duplication would be unsafe or meaningless.
  • If the compiler complains about copying: think “Do I need copy? Or do I need to redesign to borrow/consume instead?”