Swift 5.9 introduced the new ~Copyable
protocol, which makes an entity "non-copyable." By default, Swift automatically adds the Copyable
protocol to all types—without it, value types wouldn't work!
This implicit conformance simplifies our day-to-day work, allowing us to ignore copying mechanics in most cases. However, there are situations where this automatic behavior can lead to subtle bugs in our code.
~Copyable
~Copyable
conformance tells the compiler that we do not want our entity to be copied without explicit control. It forces the user to clearly define the owner of the data, ensuring that the compiler enforces strict ownership rules at compile time.
Problem
Consider following code:
struct FileHandleWrapper {
let handle: UnsafeMutablePointer<FILE>
init(path: String, mode: String) {
self.handle = fopen(path, mode)
}
func write(_ text: String) {
fputs(text, handle)
}
}
Our structure holds a pointer to a file and provides the ability to write to it — simple as that. Now, imagine you’ve created a file handler and want to write something:
func duplicateHandleProblem() {
let file1 = FileHandleWrapper(path: "test.txt", mode: "w")
let file2 = file1
file1.write("Hello, ")
file2.write("world!")
}
The code inside duplicateHandleProblem
creates a file handler, copies it to file2
, and then writes to what appears to be two separate files. However, as Swift developers, we know that our structure was copied in memory — along with the file pointer. This means both instances are writing to the same file!
One way to prevent this issue is to always remember that FileHandleWrapper
is a struct and avoid unintended copies. However, we can take this a step further by letting the compiler enforce this constraint, ensuring such problems never occur.
Fixing problem
We will modify our code by adding ~Copyable
:
struct FileHandleWrapper: ~Copyable {
let handle: UnsafeMutablePointer<FILE>
init(path: String, mode: String) {
guard let file = fopen(path, mode) else {
fatalError("Failed to open file")
}
self.handle = file
}
func write(_ text: String) {
fputs(text, handle)
}
deinit {
print("Closing file handle")
fclose(handle)
}
}
Our struct now conforms to ~Copyable
. As a result, the duplicateHandleProblem
function will no longer compile! Instead, we’ll encounter a new error: 'file1' used after consume'
.
This happens because assigning file1
to file2
"consumes" file1
, transferring ownership. Since file1
no longer holds its underlying value, attempting to use it afterward results in a compilation error.
Additional bonus
Since the FileHandleWrapper
struct now has a closely monitored lifecycle, with a well-defined point of memory deallocation, we can implement a deinit
method. This allows us to conveniently close the file when the instance is no longer needed.
New keywords
Swift 5.9 adds new keywords, that can be used with ~Copyable
protocol:
-
borrow
– Temporary Read-Only Access
Theborrow
modifier allows you to temporarily read a move-only value without taking ownership. The original value remains usable after the function call. consume
– Take Full Ownership and Invalidate the Original
Theconsume
modifier moves ownership of the value into a function, meaning the original reference becomes invalidafter the call.inout
– Temporary Mutable Access Without Moving Ownership
Theinout
modifier allows a function to temporarily modify a value but not take ownership. The original remains usable after the function.
Modifier | Ownership Taken? | Can Modify? | Value Usable After? | Use Case |
---|---|---|---|---|
borrow |
❌ No | ❌ No | ✅ Yes | Read-only access |
consume |
✅ Yes | ✅ Yes | ❌ No | Taking full ownership |
inout |
❌ No | ✅ Yes | ✅ Yes | Mutating without moving |
I hope you learned something new! Thank you for reading!