DEV Community

ArshTechPro
ArshTechPro

Posted on

Building Safer Swift Code with Noncopyable Types

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
arshtechpro profile image
ArshTechPro

Noncopyable types enforce single ownership at compile time, preventing accidental duplication of resources that should remain unique.