In-place property access
borrow and mutate accessors
Swift 6.4 introduces borrowing accessors for computed properties and subscripts. They let an API provide temporary access to existing storage instead of returning and replacing independent values.
How get and set work
Consider a container that exposes a large stored value through a computed property:
struct Storage {
var numbers: [Int]
init(elementCount: Int) {
numbers = Array(repeating: 0, count: elementCount)
}
}
struct CopyingBox {
private var storage: Storage
init(_ value: Storage) {
storage = value
}
var value: Storage {
get { storage }
set { storage = newValue }
}
}
A get accessor produces a value for the caller to use. Depending on the type, the operation, and compiler optimizations, producing that value may require a copy.
A set accessor receives a new value and replaces the previous one. When code modifies part of a computed property:
box.value.numbers[index] += 1
the operation can be understood conceptually as a read-modify-write cycle:
var temporary = box.value
temporary.numbers[index] += 1
box.value = temporary
The getter produces the value, the caller modifies a temporary, and the setter stores the updated value.
This model is useful and normally performs well. Standard-library types such as Array, String, and Dictionary can share internal storage through copy-on-write, and the optimizer can eliminate some unnecessary copies. However, when a large value must actually be copied, a small mutation can become unexpectedly expensive.
Measuring the read-modify-write cost
The following benchmark stores 400 million Int values (approximately 3.2 GB on a 64-bit platform) and performs 50 small mutations:
@inline(never)
func testGetSet(
_ box: inout CopyingBox,
iterations: Int
) {
let count = box.value.numbers.count
for iteration in 0..<iterations {
box.value.numbers[iteration % count] += 1
}
}
let elementCount = 400_000_000
let iterations = 50
var copyingBox = CopyingBox(
Storage(elementCount: elementCount)
)
let getSetTime = measure {
testGetSet(
©ingBox,
iterations: iterations
)
}
@inline(never) and the access pattern are intentional: they make the read-modify-write behavior easier to observe instead of allowing the optimizer to collapse the entire benchmark into a simpler operation.
One run produced:
Stored value: 400,000,000 Ints (~3,200 MB)
Small mutations: 50
get / set: 6.80 s
Note: This is an illustrative microbenchmark, not a universal prediction. Results depend on the compiler, optimization settings, hardware, memory pressure, and surrounding code.
The 3.2 GB input also requires substantial memory, especially while both benchmark variants and temporary copies are alive.
Borrowing the existing storage
The same property can use borrow and mutate:
struct BorrowingBox {
private var storage: Storage
init(_ value: Storage) {
storage = value
}
var value: Storage {
borrow {
return storage
}
mutate {
return &storage
}
}
}
The caller still uses the property normally:
box.value.numbers[index] += 1
What changes is how the property provides access.
borrow
borrow provides temporary, read-only access to the value already stored inside the container:
borrow {
return storage
}
It does not produce an independent value for the caller. While that borrow is active, Swift’s exclusivity rules prevent incompatible mutations of the container.
mutate
mutate provides temporary, exclusive read-write access to the original storage:
mutate {
return &storage
}
The ampersand indicates that the accessor exposes the stored value for in-place mutation. This access behaves similarly to an inout access: no other code can access the same storage incompatibly until the mutation ends.
Using the same benchmark structure:
@inline(never)
func testBorrowMutate(
_ box: inout BorrowingBox,
iterations: Int
) {
let count = box.value.numbers.count
for iteration in 0..<iterations {
box.value.numbers[iteration % count] += 1
}
}
one run produced:
get / set: 6.80 s
borrow / mutate: 0.29 µs
The important distinction is not the exact ratio. With get and set, avoiding a copy may depend on optimization. With borrow and mutate, access to the existing storage is part of the property’s semantics.
Accessor rules
A property that declares mutate must also declare borrow:
var value: Storage {
borrow { storage }
mutate { &storage }
}
Swift does not allow write-only properties, and matching read and write access scopes give callers consistent behavior.
A borrowing property cannot mix borrow with another read accessor such as get or yielding borrow. Similarly, mutate cannot coexist with yielding mutate or yielding borrow on the same property.
Borrowing accessors can also be protocol requirements:
protocol BorrowingContainer {
associatedtype Element
var element: Element {
borrow mutate
}
}
They can be used for computed properties and subscripts, and they enable access to noncopyable values that a regular getter could not return by copying.
Noncopyable values
Borrowing accessors are not only a performance optimization. They can expose a noncopyable value that a regular getter cannot return.
Consider a resource whose ownership must remain unique:
struct Resource: ~Copyable {
var id: Int
}
A container storing Resource must also be declared ~Copyable. The following getter does not compile because producing its result would require copying the stored resource:
struct InvalidContainer: ~Copyable {
private var storage: Resource
init(resource: consuming Resource) {
storage = resource
}
var resource: Resource {
get {
return storage
// Error: A getter would need to copy
// the noncopyable stored value.
}
}
}
Using borrow allows callers to inspect the existing resource without copying or consuming it. Adding mutate also allows exclusive in-place modification:
struct ResourceContainer: ~Copyable {
private var storage: Resource
init(resource: consuming Resource) {
storage = resource
}
var resource: Resource {
borrow {
return storage
}
mutate {
return &storage
}
}
}
The initializer uses consuming to transfer ownership of the resource into the container. After that, the property lends temporary access while the container remains the owner:
var container = ResourceContainer(
resource: Resource(id: 42)
)
print(container.resource.id) // Read through borrow
container.resource.id = 100 // Modify through mutate
This makes borrow and mutate an expressivity feature as well as a performance feature: they let APIs provide safe access to values that cannot be copied at all.
Stable storage is required
A borrowing accessor can only expose a value whose lifetime Swift can guarantee for the duration of the access.
Returning an existing stored property is valid:
struct Container {
private var storage: Storage
var value: Storage {
borrow {
return storage
}
}
}
Returning a local or newly constructed temporary is not:
var value: Storage {
borrow {
let temporary = Storage(elementCount: 100)
return temporary // Error: local value does not outlive the accessor
}
}
var anotherValue: Storage {
borrow {
return Storage(elementCount: 100) // Error: temporary value
}
}
A regular get accessor remains the correct choice when a property needs to calculate or construct a new value.
Exclusivity
Borrowing is safe because Swift restricts conflicting access while the loan is active.
During a read-only borrow, the owner cannot be mutated incompatibly:
use(container.value)
func use(_ value: borrowing Storage) {
// `value` can be read here.
// The owning container cannot be mutated while this access is active.
}
During a mutable borrow, access is exclusive:
modify(&container.value)
func modify(_ value: inout Storage) {
value.numbers[0] += 1
// No competing access to the same container is allowed here.
}
For a borrowing subscript, the access applies to the entire containing value. For example, two simultaneous mutable accesses to different elements of the same value are not allowed:
swap(&container[0], &container[1]) // Error: overlapping access
When to use them
borrow and mutate do not replace get and set. Traditional accessors remain simpler and more flexible for most properties, especially when values are small, copies are inexpensive, or the property constructs a result dynamically.
Borrowing accessors are most useful when:
- a container exposes a large stored value;
- repeated read-modify-write operations cause measurable copying;
- a collection or wrapper needs to expose noncopyable elements;
- in-place access is important in performance-sensitive or embedded code;
- an API should guarantee storage access instead of relying on the optimizer to remove copies.
Measure real code before changing an existing API. Switching between traditional and borrowing accessors can also affect source and ABI compatibility.
Current limitations
The most important restrictions in Swift 6.4 are:
- The returned value must come from stable storage; it cannot be a local or constructed temporary.
borrowandmutatecannot currently implement properties of classes or actors.- A mutable global variable cannot be borrowed or mutated through these accessors.
- A
mutateaccessor must be accompanied byborrow. - Borrowed and mutable accesses remain subject to Swift’s exclusivity rules.
- The current implementation does not support every control-flow shape, including accessors with multiple
returnstatements.
Classes and actors require runtime exclusivity checks both before and after a property access. Borrowing accessors do not provide a way to execute accessor code after the client’s access ends, so they cannot currently satisfy that requirement.
Summary
get produces a value
borrow lends read-only access to an existing value
set replaces a value
mutate lends exclusive access to modify an existing value in place
For ordinary properties, continue using get and set. Reach for borrow and mutate when copying is expensive or impossible and the property can safely expose stable storage.