Highlight
Swift introduced noncopyable types last year (marked with
~Copyable), letting you define types that âcannot be copied.â This yearâs session focuses on âhow to consume noncopyable types in real projectsââwhat to do when your generic parameters, protocols, and extensions encounter noncopyable types.
Core Content
Swift assumes all types can be copied by default. Thatâs a reasonable defaultâvalue types make code easier to understand; you can freely pass values without worrying that a modification somewhere else affects another place. But in some scenarios, copying causes problems.
The session uses a bank transfer (BankTransfer) example to show copying risks. Suppose you have a class-type BankTransfer with a run() method that executes the transfer. In a schedule function, if delay is less than 1 second it runs immediately, but the developer forgets to return, causing the transfer to run twice. Worse, if the sleep task is cancelled, the thrown error may be ignored, so the transfer isnât cancelled. Even if you add cancel logic in deinit, the caller holding a copy prevents it from running.
Last yearâs Swift 5.9 introduced ~Copyable to solve this. After changing BankTransfer to a noncopyable struct, the compiler prevents copying. Marking run() as consuming means the value no longer exists after the call. The compiler can directly catch the bug in the schedule functionâforgetting return causes transfer to be consumed twice.
This yearâs focus is making noncopyable types work in generic code. Previous limitations: generic parameters default to requiring Copyable types, and protocols default to inheriting Copyable, preventing noncopyable types from participating in generic programming. Swift 6 made three key changes: generic parameters can be declared ~Copyable, noncopyable types can have extensions, and noncopyable types can conform to protocols. This turns ~Copyable from an experimental feature into a tool usable in production code.
The session also demonstrates conditional copyableâa generic type can decide whether itâs Copyable based on whether its parameter type is Copyable. For example, Job<Action> can be Copyable as long as Action is Copyable.
Detailed Content
Copying Mechanism Review
Swift has two kinds of copying: value copy and reference copy. Illustrated with a Player example (0:52):
struct Player {
var icon: String
}
func test() {
let player1 = Player(icon: "đ¸")
var player2 = player1
player2.icon = "đ"
assert(player1.icon == "đ¸")
}
Key points:
- struct is a value type;
player2 = player1copies all data - Modifying
player2.icondoesnât affectplayer1because theyâre independent copies
If itâs a class (1:55):
class PlayerClass {
var icon: String
init(_ icon: String) { self.icon = icon }
}
func test() {
let player1 = PlayerClass("đ¸")
let player2 = player1
player2.icon = "đ"
assert(player1.icon == "đ¸")
}
Key points:
- class is a reference type;
player2 = player1only copies the reference (shallow copy) - Both variables point to the same object; modifying
player2.iconaffectsplayer1
Why Noncopyable Is Needed
Copying is sometimes dangerous. The session uses a bank transfer example (5:10):
class BankTransfer {
var complete = false
func run() {
assert(!complete)
// .. do it ..
complete = true
}
deinit {
if !complete { cancel() }
}
func cancel() { /* ... */ }
}
func schedule(_ transfer: BankTransfer,
_ delay: Duration) async throws {
if delay < .seconds(1) {
transfer.run()
}
try await Task.sleep(for: delay)
transfer.run()
}
func startPayment() async {
let payment = BankTransfer()
log.append(payment)
try? await schedule(payment, .seconds(3))
}
let log = Log()
final class Log: Sendable {
func append(_ transfer: BankTransfer) { /* ... */ }
}
Key points:
scheduleruns immediately when delay < 1 second but forgets to return, causing the transfer to run twice- Even with the
completeflag and assertion, it may not trigger at runtime log.append(payment)holds a copy of transfer, preventing deinit from running- If sleep is cancelled, transfer isnât cancelled
Noncopyable Type Basics
Declare a non-copyable type with ~Copyable (7:46):
struct FloppyDisk: ~Copyable {}
func copyFloppy() {
let system = FloppyDisk()
let backup = consume system
load(system)
// ...
}
func load(_ disk: borrowing FloppyDisk) {}
Key points:
~Copyablesuppresses the default Copyable conformance- The
consumekeyword consumes the variableâs value, leaving an uninitialized state - Attempting to read a consumed variable is a compile error
Noncopyable type parameters must declare ownership (8:18):
struct FloppyDisk: ~Copyable { }
func newDisk() -> FloppyDisk {
let result = FloppyDisk()
format(result)
return result
}
func format(_ disk: consuming FloppyDisk) {
// ...
}
Key points:
consumingmeans the function takes ownership of the parameter- format returns nothing, so
newDiskfails to compile
Three ownership modifiers (9:00):
// consuming: take ownership.
func format(_ disk: consuming FloppyDisk) {
// ...
}
// borrowing: read-only borrow.
func format(_ disk: borrowing FloppyDisk) {
var tempDisk = disk
// ...
}
// inout: mutable borrow.
func format(_ disk: inout FloppyDisk) {
var tempDisk = disk
// ...
disk = tempDisk
}
Key points:
consumingtakes ownershipâthe caller canât use the value afterwardborrowingprovides read-only access, similar to a let bindinginoutprovides writable access but must reinitialize the parameter before returning
Noncopyable BankTransfer
Rewrite BankTransfer as noncopyable (10:28):
struct BankTransfer: ~Copyable {
consuming func run() {
// .. do it ..
discard self
}
deinit {
cancel()
}
consuming func cancel() {
// .. do the cancellation ..
discard self
}
}
Key points:
consuming func run()means calling run consumes this valuediscard selfdestroys self without calling deinit- The compiler guarantees run canât be called twice
schedule function (11:10):
func schedule(_ transfer: consuming BankTransfer,
_ delay: Duration) async throws {
if delay < .seconds(1) {
transfer.run()
return
}
try await Task.sleep(for: delay)
transfer.run()
}
Key points:
consuming BankTransfermeans schedule takes ownership of transfer- Forgetting return causes a compile errorâtransfer would be consumed twice
- If sleep is cancelled, transfer is destroyed and deinit runs cancel
Noncopyable Generics
Before Swift 6, generic parameters default to requiring Copyable. Now you can use ~Copyable to remove this constraint (15:50):
protocol Runnable: ~Copyable {
consuming func run()
}
struct Command: Runnable {
func run() { /* ... */ }
}
struct BankTransfer: ~Copyable, Runnable {
consuming func run() { /* ... */ }
}
func execute2<T>(_ t: T)
where T: Runnable {
t.run()
}
func execute3<T>(_ t: consuming T)
where T: Runnable,
T: ~Copyable {
t.run()
}
func test() {
execute2(Command())
execute2(BankTransfer()) // expected error: 'execute2' requires that 'BankTransfer' conform to 'Copyable'
execute3(Command())
execute3(BankTransfer())
}
Key points:
execute2âs generic parameter T defaults to requiring Copyable, so it canât accept BankTransferexecute3usesT: ~Copyableto remove this constraint, accepting both types~Copyableconstraint âwidensâ the type space rather than narrowing it
Conditional Copyable
A type can decide whether itâs Copyable based on whether its generic parameter is Copyable (18:05):
struct Job<Action: Runnable & ~Copyable>: ~Copyable {
var action: Action?
}
func runEndlessly(_ job: consuming Job<Command>) {
while true {
let current = copy job
current.action?.run()
}
}
extension Job: Copyable where Action: Copyable {}
protocol Runnable: ~Copyable {
consuming func run()
}
struct Command: Runnable {
func run() { /* ... */ }
}
Key points:
Jobitself is~Copyablebecause it needs to store a noncopyable Actionextension Job: Copyable where Action: Copyablemakes Job Copyable when Action is CopyableJob<Command>is Copyable;Job<BankTransfer>is not
Extension Default Constraints
Extensions on noncopyable types default to applying only when generic parameters are Copyable (19:27):
extension Job {
func getAction() -> Action? {
return action
}
}
func inspectCmd(_ cmdJob: Job<Command>) {
let _ = cmdJob.getAction()
let _ = cmdJob.getAction()
}
func inspectXfer(_ transferJob: borrowing Job<BankTransfer>) {
let _ = transferJob.getAction() // expected error: method 'getAction' requires that 'BankTransfer' conform to 'Copyable'
}
Key points:
- Regular extensions default to assuming generic parameters are Copyable
getAction()returning action requires copying, so it only works for Copyable Action- Calling
getAction()onJob<BankTransfer>errors
To make an extension apply in all cases, declare explicitly (20:14):
protocol Cancellable {
mutating func cancel()
}
extension Job: Cancellable {
mutating func cancel() {
action = nil
}
}
Key points:
- This extension defaults to applying only when Action is Copyable
- To make Cancellable apply to all Job, the protocol itself also needs to be
~Copyable
Making the extension apply in all cases (21:00):
protocol Cancellable: ~Copyable {
mutating func cancel()
}
extension Job: Cancellable where Action: ~Copyable {
mutating func cancel() {
action = nil
}
}
Key points:
- After declaring the protocol as
~Copyable, conformance no longer requires the type to be Copyable where Action: ~Copyablemakes this extension apply to all Action, Copyable or not
Core Takeaways
-
Wrap exclusive resources with
~Copyable- File handles, network connections, database transactions, and encryption keys naturally suit noncopyable type wrapping
- The compiler ensures resources arenât accidentally copied, and deinit always runs when the âlast userâ leaves
- Start by picking a resource management type (like
FileHandle), change it to a~Copyable struct, and do cleanup in deinit
-
Introduce generic support incrementally
- You donât need to change all generic code to support noncopyable at once
- Start with new generic functions: use
<T: ~Copyable>instead of<T>so both type kinds work - Keep existing generic code unchanged; modify when you actually need noncopyable support
-
Use conditional Copyable for flexibility
- If your generic type is just a âcontainer,â consider conditional Copyable
extension MyType: Copyable where T: Copyablelets your type be copied when T is Copyable- Callers writing only Copyable code donât need changes, while noncopyable types can still use it
Related Sessions
- Analyze heap memory â Deep dive into heap memory, understanding value and reference type memory layout
- Explore Swift performance â How Swift balances abstraction and performance, including copy semantics performance impact
- Explore the Swift on Server ecosystem â Swift server application scenarios; noncopyable types are useful for server resource management
- Go further with Swift Testing â Swift Testing framework; when testing noncopyable types, you can leverage consuming semantics
Comments
GitHub Issues ¡ utterances