Memory Safety
By default Swift prevents unsafe behavior from happening in your code. It won't let you read uninitialized memory, it bounds-checks arrays, and it guarantees that memory is initialized before use. Among these guarantees, Swift also ensures that multiple accesses to the same region of memory don't conflict. This chapter explains what a conflicting access is and how to avoid one.
Understanding conflicting access to memory
Access to memory happens whenever you do things like set the value of a variable or pass an argument to a function. For example, this code performs both a write access and a read access:
var one = 1 // a write access to one
print("We're number \(one)!") // a read access to one
Most accesses are instantaneous and never overlap.
A conflicting access can occur when different parts of your code try to access the same location in memory at the same time. Multiple accesses to a location at the same time can produce unpredictable or inconsistent behavior. In Swift, there are ways to modify a value that span several lines of code, making it possible to attempt to access a value in the middle of its own modification.
Note: if you've written concurrent or multithreaded code,
conflicting access to memory might be a familiar problem. However, the
conflicting access discussed here can happen on a single thread and
doesn't involve concurrency. If a conflict happens across threads or tasks,
Swift 6's strict concurrency checking and the Sendable rules
catch those at compile time instead.
Characteristics of memory access
There are three characteristics of a memory access to consider in the context of conflicting access: whether the access is a read or a write, the duration of the access, and the location in memory being accessed. Specifically, a conflict occurs if you have two accesses that meet all of the following conditions:
- At least one is a write access or a nonatomic access.
- They access the same location in memory.
- Their durations overlap.
The difference between a read and a write access is usually obvious: a write changes the location in memory, but a read doesn't. The location refers to what's being accessed — a variable, constant, or property.
The duration of a memory access is either instantaneous or long-term. An access is instantaneous if no other code can run after that access starts but before it ends. By their nature, two instantaneous accesses can't happen at the same time. Most accesses are instantaneous:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber) // instantaneous read then write
print(myNumber) // → 2
Instantaneous accesses don't overlap, so there's no conflict.
However, several kinds of access — called long-term accesses — span the execution of other code. The difference is that other code can run after a long-term access starts but before it ends, which is called overlap. A long-term access can overlap with other long-term accesses and with instantaneous accesses. Overlapping accesses appear primarily in code that uses in-out parameters or mutating methods of a structure, as the next sections show.
Conflicting access to in-out parameters
A function has long-term write access to all of its in-out parameters. The write access starts after all of the non-in-out parameters have been evaluated and lasts for the entire duration of that function call. If there are multiple in-out parameters, the write accesses start in the same order as the parameters appear.
One consequence of this is that you can't access the original variable that was passed as in-out, even if scoping rules and access control would otherwise permit it — any access to the original creates a conflict:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize // read access to stepSize during the in-out write
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
stepSize is both the long-term write target and read inside the function.
Here stepSize is a global variable, and it's normally
accessible from inside increment(_:). But the write access to
number overlaps the read access to stepSize — both
refer to the same memory location. One way to resolve the conflict is to
make an explicit copy:
var copyOfStepSize = stepSize
increment(©OfStepSize)
stepSize = copyOfStepSize // safe: distinct locations, no overlap
Copying first ensures the read and the write touch different memory.
Another consequence is that passing a single variable as the argument for multiple in-out parameters of the same function produces a conflict:
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOne = 42
var playerTwo = 30
balance(&playerOne, &playerTwo) // OK — different locations
balance(&playerOne, &playerOne) // Error: conflicting accesses to playerOne
The same variable can't satisfy two overlapping in-out write accesses.
Conflicting access to self in methods
A mutating method on a structure has write access to
self for the duration of the method call. Consider a game where
each player has health and energy, and topping up one player draws energy
from another:
struct Player {
var name: String
var health: Int
var energy: Int
mutating func restoreHealth() {
health = 100 // write access to self for the whole method
}
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK — oscar and maria are distinct
oscar.shareHealth(with: &oscar) // Error: conflicting accesses to oscar
Calling shareHealth with oscar's own value conflicts with the method's write access to self.
The first call is fine: oscar and maria are
different memory locations, so their write accesses don't overlap. The
second call fails: shareHealth(with:) needs a write access to
self (which is oscar) and a write access
to teammate (also oscar) at the same time — two
overlapping write accesses to the same location.
Conflicting access to properties
Types like structures, tuples, and enumerations are made up of individual constituent values, such as the properties of a structure or the elements of a tuple. Because these are value types, mutating any piece of the value mutates the whole value — which means read or write access to one property requires access to the whole value. For example, overlapping write accesses to the elements of a tuple produce a conflict:
var playerInfo = (health: 10, energy: 20)
balance(&playerInfo.health, &playerInfo.energy)
// Error: conflicting access to properties of playerInfo
Both arguments require overlapping write access to the whole tuple.
The same restriction applies to the stored properties of a structure stored in a global variable:
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)
// Error: conflicting access to properties of holly
Overlapping write access to two properties of the same global structure.
In practice, most access to the properties of a structure can overlap safely. The compiler can prove that overlapping access to properties of a structure is safe — meaning it allows it — if all of these hold:
- You're accessing only stored properties of an instance, not computed properties or class properties.
- The structure is the value of a local variable, not a global variable.
- The structure is either not captured by any closures, or it's captured only by non-escaping closures.
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK — local, stored, not escaping
}
When the compiler can prove exclusivity locally, overlapping property access is allowed.
Tip: if the compiler can't guarantee safety, it rejects the access. The conditions above are exactly the situations where it can prove that two properties refer to disjoint storage. When you hit one of these errors, the simplest fix is usually to copy the value into a local variable, perform the work, and copy it back.
Wrapping up
Swift's exclusive-access rule — at most one write, or any number of reads,
to a given memory location at one time — is what keeps your single-threaded
code free of surprising aliasing bugs. Most of the time these checks are
invisible because instantaneous accesses can't overlap. They surface mainly
around inout parameters and mutating methods, and
the fix is almost always to give each access its own copy of the value.