What Are Noncopyable Types?
In Swift, most types are copyable by default. When you assign one variable to another or pass a value to a function, Swift creates a copy. However, some resources should have exactly one owner - think of a file handle, database connection, or unique system resource.
Noncopyable types enforce single ownership at compile time, preventing accidental duplication of resources that should remain unique.
The Problem They Solve
Consider managing a file handle. With regular Swift types, you might accidentally create multiple references to the same file, leading to bugs when the file is closed multiple times. Noncopyable types prevent this at compile time.
Basic Syntax
struct FileHandler: ~Copyable {
private let fileDescriptor: Int32
init(path: String) throws {
fileDescriptor = open(path, O_RDONLY)
guard fileDescriptor >= 0 else {
throw FileError.cannotOpen
}
}
deinit {
close(fileDescriptor)
}
}
The ~Copyable
constraint means "not copyable". This type:
- Has value semantics but cannot be copied
- Guarantees single ownership
- Automatically cleans up resources
- Provides compile-time safety
Core Concepts
Ownership and Movement
With noncopyable types, you don't copy values - you move them:
var file1 = try FileHandler(path: "data.txt")
var file2 = file1 // Error: Cannot copy noncopyable type
// To transfer ownership, you must use it:
func processFile(_ file: FileHandler) {
// Function now owns the file
}
processFile(file1) // file1 is no longer valid after this
Borrowing vs Consuming
Swift provides two ways to work with noncopyable values:
Borrowing: Temporary access without taking ownership
Consuming: Taking ownership permanently
Practical Examples
Example 1: Database Transaction
A database transaction is a perfect candidate for noncopyable types. You want exactly one owner who can commit or rollback:
struct DatabaseTransaction: ~Copyable {
private let connection: DatabaseConnection
private let transactionID: String
private var isActive = true
init(connection: DatabaseConnection) throws {
self.connection = connection
self.transactionID = try connection.beginTransaction()
}
// Borrowing: Check status without consuming
borrowing func executeQuery(_ sql: String) throws -> QueryResult {
guard isActive else {
throw DatabaseError.transactionInactive
}
return try connection.execute(sql, in: transactionID)
}
// Consuming: Commit consumes the transaction
consuming func commit() throws {
try connection.commit(transactionID)
isActive = false
}
// Consuming: Rollback also consumes the transaction
consuming func rollback() throws {
try connection.rollback(transactionID)
isActive = false
}
deinit {
// Auto-rollback if not committed
if isActive {
try? connection.rollback(transactionID)
}
}
}
// Usage:
func updateUserData() throws {
var transaction = try DatabaseTransaction(connection: db)
// Can execute multiple queries (borrowing)
try transaction.executeQuery("UPDATE users SET active = true WHERE id = 1")
try transaction.executeQuery("INSERT INTO logs (action) VALUES ('user_activated')")
// Must explicitly commit or rollback (consuming)
try transaction.commit()
// transaction is no longer usable here
}
Example 2: Unique Network Request Token
For network requests, you might want a token that can only be used once:
struct RequestToken: ~Copyable {
private let id: String
private let endpoint: URL
init(endpoint: URL) {
self.id = UUID().uuidString
self.endpoint = endpoint
}
// Borrowing: Check token info without consuming
borrowing func info() -> (id: String, endpoint: URL) {
return (id, endpoint)
}
// Consuming: Using the token invalidates it
consuming func performRequest() async throws -> Data {
var request = URLRequest(url: endpoint)
request.setValue(id, forHTTPHeaderField: "X-Request-Token")
let (data, _) = try await URLSession.shared.data(for: request)
return data
}
}
// Usage:
func fetchUserData() async throws {
let token = RequestToken(endpoint: URL(string: "https://api.example.com/user")!)
// Can inspect token (borrowing)
print("Request ID: \(token.info().id)")
// Perform request (consuming)
let userData = try await token.performRequest()
// token is no longer valid - prevents accidental reuse
}
Example 3: Generic Container with Conditional Copyability
Sometimes you want a container that's only copyable when its contents are copyable:
struct SecureBox<T: ~Copyable>: ~Copyable {
private var content: T
init(_ content: consuming T) {
self.content = content
}
// Borrowing: Read access
borrowing func peek<R>(_ operation: (borrowing T) -> R) -> R {
operation(content)
}
// Consuming: Take the content out
consuming func unwrap() -> T {
content
}
}
// The box becomes copyable when T is copyable
extension SecureBox: Copyable where T: Copyable {}
// Usage with noncopyable type:
let fileBox = SecureBox(try FileHandler(path: "data.txt"))
// let copy = fileBox // Error: Cannot copy
// Usage with copyable type:
let stringBox = SecureBox("Hello")
let copy = stringBox // OK: String is copyable, so SecureBox<String> is copyable
Key Takeaways
When to Use Noncopyable Types
Perfect for:
- System resources (files, sockets, locks)
- Unique tokens or sessions
- Transactions that must complete exactly once
- RAII (Resource Acquisition Is Initialization) patterns
Avoid for:
- Simple data that needs to be shared
- Types used frequently in collections
- When copyability is a natural expectation
Conclusion
Noncopyable types provide compile-time guarantees about resource ownership, eliminating entire classes of bugs. While they require thinking differently about value movement, the safety and performance benefits make them invaluable for systems programming and resource management in Swift.
Top comments (1)
Noncopyable types enforce single ownership at compile time, preventing accidental duplication of resources that should remain unique.