← All topics/Swift language features

Question 9

Sendable, ownership, and mutability

What does Sendable communicate to the compiler? How does it relate to actor isolation and mutability in Swift 6 concurrency?

Answer outline

Sendable marks a type whose values can safely cross a concurrency boundary without introducing a data race. That boundary might be an actor, a new task, or a concurrent closure.

Structs and enums whose stored properties are all Sendable get the conformance implicitly, except public types, which must declare it. Classes must prove safety themselves, usually by being final with only immutable Sendable properties. Otherwise they can adopt @unchecked Sendable to opt out of compile-time checking.

In Swift 6 language mode, passing a non-Sendable value across a concurrency boundary is a compile-time error. The compiler enforces isolation instead of leaving data-race safety to runtime.

Parameter modifiers (borrowing, consuming, inout) and mutating describe how a value moves between caller and callee. They make ownership explicit and work with Swift's exclusive access rule, which forbids two overlapping accesses to the same variable when one of them writes.

Sendable conformance paths and how Swift 6 enforces the concurrency boundary at compile time.

Principles

  • Sendable values are safe to cross concurrency boundaries, and Swift 6 checks this at actor calls and task closures.
  • Non-public structs with Sendable stored properties conform automatically, while classes must prove thread safety themselves.
  • @unchecked Sendable silences the compiler, so you take full responsibility for data-race safety.
  • Prefer immutable types across concurrency boundaries, and put shared mutable state in an actor.
Borrowing parameter
func utf8Count(_ text: borrowing String) -> Int {
    text.utf8.count // borrowed for this call only, so the caller still owns text
}
Consuming parameter
func logThenDrop(_ message: consuming String) {
    print(message)
}

logThenDrop("done")
Inout parameter
func double(_ value: inout Int) {
    value *= 2
}

var x = 3
double(&x)
// x is now 6
Mutating method
struct Counter {
    var n = 0
    mutating func tick() { n += 1 }
}

var c = Counter()
c.tick() // needs a var binding so self can change