Copyable vs Noncopyable in Swift — A Friendly, Step‑by‑Step Guide
Copyable vs Noncopyable in Swift — A Friendly, Step‑by‑Step Guide
Table of Contents
- Big Picture (Why this matters)
- What “copying” means in Swift
- 2.1 Value types (struct/enum)
- 2.2 Reference types (class)
- 2.3 Implicit copies vs explicit copies
Copyableand~Copyable(noncopyable)- 3.1 The idea
- 3.2 Declaring a noncopyable type
- 3.3 Lifetime and
deinitfor noncopyable value types
- Ownership when calling functions
- 4.1
borrowing - 4.2
consuming - 4.3
inout - 4.4 The
copyandconsumeoperators - 4.5 Quick comparison table
- 4.1
- Beginner examples (Copyable types)
- Beginner → Intermediate (Declaring noncopyable)
- Intermediate (borrowing vs inout vs consuming)
- Intermediate → Advanced (methods,
deinit, and enums) - Generics, extensions, and conditional conformances
- Common pitfalls (and what the compiler says)
- Practice problems – Set A (Beginner) – Set B (Intermediate) – Set C (Advanced)
- 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 = a→bis 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 = a→aandbpoint 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 xto be explicit when needed.
3) Copyable and ~Copyable (noncopyable)
3.1 The idea
Copyableis a marker that says a type can be copied. Most types areCopyableby default.~Copyablemeans “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
selfand 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 forCopyablevalues). Often required insideborrowing/consumingfunctions 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 consumeis 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 parameterconsuming Tand make sure your algorithm doesn’t require copying. If you need duplication, constrainT: Copyableand usecopy.
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)
- 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.
- 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.
- 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.
- 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.
- 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).
- 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)usingcopy. - Otherwise, return
(nil, nil). (Hint: use an overload withT: 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
borrowingfor read‑only helpers. - Use
inoutwhen a function needs to mutate and return the same value. - Use
consumingto move ownership (single‑use, cleanup-on-drop types). - Reach for
~Copyablewhen 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?”