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(
        &copyingBox,
        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:

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:

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.

References