Noncopyable types in Swift
In Swift, types are copyable by default. This design simplifies development by allowing values to be easily duplicated when assigned to new variables or passed to functions. While convenient, this behavior can sometimes lead to unintended issues. For instance, copying a single-use ticket or duplicating a database connection could result in invalid states or resource conflicts.
To address these challenges, Swift 5.9 introduced noncopyable types. By marking a type as ~Copyable
, we explicitly prevent Swift from duplicating it. This guarantees unique ownership of the value and enforces stricter constraints, reducing the risk of errors.
Here’s a simple example of a noncopyable type:
struct SingleUseTicket: ~Copyable {
let ticketID: String
}
In contrast to the regular behavior of value types, when we assign an instance of a noncopyable type to a new variable, the value gets moved instead of being copied. If we attempt to use the original variable later, we'll get a compile-time error.
let originalTicket = SingleUseTicket(ticketID: "S645")
let newTicket = originalTicket
// Error: 'originalTicket' used after consume
// print(originalTicket.ticketID)
Classes can't be declared noncopyable. All class types remain copyable by retaining and releasing references to the object.
# Methods in noncopyable types
Methods in noncopyable types can read, mutate, or consume self
.
# Borrowing methods
Methods inside a noncopyable type are borrowing by default. This means they only have read access to the instance, allowing safe inspection of the instance without affecting its validity.
struct SingleUseTicket: ~Copyable {
let ticketID: String
func describe() {
print("This ticket is \(ticketID).")
}
}
let ticket = SingleUseTicket(ticketID: "A123")
// Prints `This ticket is A123.`
ticket.describe()
# Mutating methods
A mutating method provides temporary write access to self, allowing modifications without invalidating the instance.
struct SingleUseTicket: ~Copyable {
var ticketID: String
mutating func updateID(newID: String) {
ticketID = newID
print("Ticket ID updated to \(ticketID).")
}
}
var ticket = SingleUseTicket(ticketID: "A123")
// Prints `Ticket ID updated to B456.`
ticket.updateID(newID: "B456")
# Consuming methods
A consuming method takes ownership of self
, invalidating the instance once the method completes. This is useful for tasks that finalize or dispose of a resource. After the method is called, any attempt to access the instance results in a compile-time error.
struct SingleUseTicket: ~Copyable {
let ticketID: String
consuming func use() {
print("Ticket \(ticketID) used.")
}
}
func useTicket() {
let ticket = SingleUseTicket(ticketID: "A123")
ticket.use()
// Error: 'ticket' consumed more than once
// ticket.use()
}
useTicket()
Note that we can't consume noncopyable types stored in global variables, that's why we wrapped the code into the useTicket()
function in our example.
# Noncopyable types in function arguments
When passing noncopyable types as arguments to functions, Swift requires us to specify the ownership model for that function. We can mark parameters as borrowing
, inout
, or consuming
, each offering different levels of access, similar to methods inside the types.
# Borrowing parameters
Borrowing ownership allows the function to temporarily read the value without consuming or mutating it.
func inspectTicket(_ ticket: borrowing SingleUseTicket) {
print("Inspecting ticket \(ticket.ticketID).")
}
# Inout parameters
The inout
modifier provides temporary write access to a value, allowing the function to modify it while returning ownership to the caller.
func updateTicketID(_ ticket: inout SingleUseTicket, to newID: String) {
ticket.ticketID = newID
print("Ticket ID updated to \(ticket.ticketID).")
}
# Consuming parameters
When a parameter is marked as consuming
, the function takes full ownership of the value, invalidating it for the caller. This is ideal for tasks where the value is no longer needed after the function.
func processTicket(_ ticket: consuming SingleUseTicket) {
ticket.use()
}
# Deinitializers and the discard operator
Noncopyable structs and enums can have deinitializers, like classes, which run automatically at the end of the instance's lifetime.
struct SingleUseTicket: ~Copyable {
let ticketID: Int
deinit {
print("Ticket deinitialized.")
// cleanup logic
}
}
However, when both a consuming method and a deinitializer perform cleanup, there is a risk of redundant operations. To address this, Swift introduced the discard
operator.
By using discard self
in a consuming method, we explicitly stop the deinitializer from being called, avoiding duplicate logic:
struct SingleUseTicket: ~Copyable {
let ticketID: Int
consuming func invalidate() {
print("Ticket \(ticketID) invalidated.")
// cleanup logic
discard self
}
deinit {
print("Ticket deinitialized.")
// cleanup logic
}
}
Note that we can only use discard
if our type contains trivially-destroyed stored properties. It can't have reference-counted, generic, or existential fields.
Noncopyable types are invaluable in scenarios where unique ownership is essential. They prevent duplication of resources like single-use tokens, cryptographic keys, or database connections, reducing the risk of errors. By enforcing ownership rules at compile time, Swift enables developers to write safer, more efficient code.
While noncopyable types might not be required in every project, they provide a powerful tool for ensuring safety and clarity in critical systems. As Swift continues to evolve, these types represent a significant step forward in the language’s focus on performance and correctness.
As someone who has worked extensively with Swift, I've gathered many insights over the years. I've compiled them in my book Swift Gems, which is packed with advanced tips and techniques to help intermediate and advanced Swift developers enhance their coding skills. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice to elevate your Swift development. Grab your copy of Swift Gems and let's explore the advanced techniques together.