| name | ownership-conventions |
| description | Use when optimizing large value type performance, working with noncopyable types, or reducing ARC traffic. Covers borrowing, consuming, inout modifiers, consume operator, ~Copyable types. |
| skill_type | discipline |
| version | 1.0.0 |
borrowing & consuming — Parameter Ownership
Explicit ownership modifiers for performance optimization and noncopyable type support.
When to Use
✅ Use when:
- Large value types being passed read-only (avoid copies)
- Working with noncopyable types (
~Copyable) - Reducing ARC retain/release traffic
- Factory methods that consume builder objects
- Performance-critical code where copies show in profiling
❌ Don't use when:
- Simple types (Int, Bool, small structs)
- Compiler optimization is sufficient (most cases)
- Readability matters more than micro-optimization
- You're not certain about the performance impact
Quick Reference
| Modifier | Ownership | Copies | Use Case |
|---|---|---|---|
| (default) | Compiler chooses | Implicit | Most cases |
borrowing |
Caller keeps | Explicit copy only |
Read-only, large types |
consuming |
Caller transfers | None needed | Final use, factories |
inout |
Caller keeps, mutable | None | Modify in place |
Default Behavior by Context
| Context | Default | Reason |
|---|---|---|
| Function parameters | borrowing |
Most params are read-only |
| Initializer parameters | consuming |
Usually stored in properties |
| Property setters | consuming |
Value is stored |
Method self |
borrowing |
Methods read self |
Patterns
Pattern 1: Read-Only Large Struct
struct LargeBuffer {
var data: [UInt8] // Could be megabytes
}
// ❌ Default may copy
func process(_ buffer: LargeBuffer) -> Int {
buffer.data.count
}
// ✅ Explicit borrow — no copy
func process(_ buffer: borrowing LargeBuffer) -> Int {
buffer.data.count
}
Pattern 2: Consuming Factory
struct Builder {
var config: Configuration
// Consumes self — builder invalid after call
consuming func build() -> Product {
Product(config: config)
}
}
let builder = Builder(config: .default)
let product = builder.build()
// builder is now invalid — compiler error if used
Pattern 3: Explicit Copy in Borrowing
With borrowing, copies must be explicit:
func store(_ value: borrowing LargeValue) {
// ❌ Error: Cannot implicitly copy borrowing parameter
self.cached = value
// ✅ Explicit copy
self.cached = copy value
}
Pattern 4: Consume Operator
Transfer ownership explicitly:
let data = loadLargeData()
process(consume data)
// data is now invalid — compiler prevents use
Pattern 5: Noncopyable Type
For ~Copyable types, ownership modifiers are required:
struct FileHandle: ~Copyable {
private let fd: Int32
init(path: String) throws {
fd = open(path, O_RDONLY)
guard fd >= 0 else { throw POSIXError.errno }
}
borrowing func read(count: Int) -> Data {
// Read without consuming handle
var buffer = [UInt8](repeating: 0, count: count)
_ = Darwin.read(fd, &buffer, count)
return Data(buffer)
}
consuming func close() {
Darwin.close(fd)
// Handle consumed — can't use after close()
}
deinit {
Darwin.close(fd)
}
}
// Usage
let file = try FileHandle(path: "/tmp/data.txt")
let data = file.read(count: 1024) // borrowing
file.close() // consuming — file invalidated
Pattern 6: Reducing ARC Traffic
class ExpensiveObject { /* ... */ }
// ❌ Default: May retain/release
func inspect(_ obj: ExpensiveObject) -> String {
obj.description
}
// ✅ Borrowing: No ARC traffic
func inspect(_ obj: borrowing ExpensiveObject) -> String {
obj.description
}
Pattern 7: Consuming Method on Self
struct Transaction {
var amount: Decimal
var recipient: String
// After commit, transaction is consumed
consuming func commit() async throws {
try await sendToServer(self)
// self consumed — can't modify or reuse
}
}
Common Mistakes
Mistake 1: Over-Optimizing Small Types
// ❌ Unnecessary — Int is trivially copyable
func add(_ a: borrowing Int, _ b: borrowing Int) -> Int {
a + b
}
// ✅ Let compiler optimize
func add(_ a: Int, _ b: Int) -> Int {
a + b
}
Mistake 2: Forgetting Explicit Copy
func cache(_ value: borrowing LargeValue) {
// ❌ Compile error
self.values.append(value)
// ✅ Explicit copy required
self.values.append(copy value)
}
Mistake 3: Consuming When Borrowing Suffices
// ❌ Consumes unnecessarily — caller loses access
func validate(_ data: consuming Data) -> Bool {
data.count > 0
}
// ✅ Borrow for read-only
func validate(_ data: borrowing Data) -> Bool {
data.count > 0
}
Performance Considerations
When Ownership Modifiers Help
- Large structs (arrays, dictionaries, custom value types)
- High-frequency function calls in tight loops
- Reference types where ARC traffic is measurable
- Noncopyable types (required, not optional)
When to Skip
- Default behavior is almost always optimal
- Small value types (primitives, small structs)
- Code where profiling shows no benefit
- API stability concerns (modifiers affect ABI)
Decision Tree
Need explicit ownership?
├─ Working with ~Copyable type?
│ └─ Yes → Required (borrowing/consuming)
├─ Large value type passed frequently?
│ ├─ Read-only? → borrowing
│ └─ Final use? → consuming
├─ ARC traffic visible in profiler?
│ ├─ Read-only? → borrowing
│ └─ Transferring ownership? → consuming
└─ Otherwise → Let compiler choose
Resources
Swift Evolution: SE-0377
WWDC: 2024-10170
Skills: swift-performance, swift-concurrency