Mutation and value semantics
Changing data in place: why Swift arrays and structures are copied instead of shared, mutating methods, inout parameters, and how that differs from Python’s lists.
- CS61A
- Mutation · Composing Programs 2.4
- Swift book
- Structures and Classes · Methods · Functions
CS61A’s Mutation lecture is full of surprises like this one, in Python:
>>> a = [1, 2]
>>> b = a
>>> b.append(3)
>>> a
[1, 2, 3]
a and b are two names for the same list, so changing it through one
name changes what the other sees. This is called aliasing, and it causes
a large share of bugs in Python programs.
Swift’s arrays, dictionaries, strings, and structures work differently.
Value semantics
What would Swift print?
var a = [1, 2]
var b = a
b.append(3)
print(a, b)Show answer and explanation
Answer[1, 2] [1, 2, 3]
In Swift, var b = a gives b its own copy of the array. Appending to b
changes only b; a is still [1, 2]. This is the opposite of the Python
example above.
A type has value semantics when assigning it, or passing it to a
function, behaves like making a copy. Int, String, Array,
Dictionary, Set, and every structure and enumeration you define have
value semantics. Two variables never secretly share one of these.
Copying a large array sounds slow, and Swift avoids actually copying until it must. The two arrays share storage until one of them is changed, and only then is the copy made. This is called copy-on-write; it keeps the simple rule without the cost.
Nested values are copies too:
What would Swift print?
var grid = [[0, 0], [0, 0]]
var row = grid[0]
row[0] = 7
grid[1][1] = 5
print(grid, row)Show answer and explanation
Answer[[0, 0], [0, 5]] [7, 0]
row is a copy of the first row, so changing it leaves grid alone.
Changing grid[1][1] directly does change grid. In Python, row would
have been an alias for the inner list, and grid would show the 7.
Where mutation is allowed
Because values are copied, Swift can check at compile time which code is allowed to change which values.
- A name bound with
letcan never be changed, including its properties and elements. - A function’s parameters are constants. Changing an array argument inside a function is a compile-time error.
What would Swift print?
func addZero(_ values: [Int]) {
values.append(0)
}
var numbers = [1]
addZero(numbers)
print(numbers)Show answer and explanation
AnswerError
values is a parameter, and parameters are constants: “cannot use mutating
member on immutable value”. The function receives a copy it cannot change.
To return a changed array, copy it into a var, change that, and return it.
mutating methods
A method that changes a structure’s properties must be marked
mutating, and it can only be called on a var:
struct Counter {
private(set) var count = 0
mutating func increment() {
count += 1
}
}
var clicks = Counter()
clicks.increment()
clicks.increment()
print(clicks.count) // 2
private(set) lets other code read count but not assign it, so the only
way to change it is through increment(). Calling increment() on a let
counter is a compile-time error: the mutating keyword is how the compiler
knows the call would change it.
inout parameters
Sometimes a function’s job really is to change its caller’s variable. An
inout parameter allows it, and the caller writes & to show the
variable may be changed:
func double(_ values: inout [Int]) {
for i in values.indices {
values[i] *= 2
}
}
var numbers = [1, 2, 3]
double(&numbers)
print(numbers) // [2, 4, 6]
The & at the call site is the point: you can see, reading the call, that
numbers is about to change. Mutation in Swift is always visible where it
happens.
What would Swift print?
func swapTwo(_ x: inout Int, _ y: inout Int) {
(x, y) = (y, x)
}
var a = 1
var b = 2
swapTwo(&a, &b)
print(a, b)Show answer and explanation
Answer2 1
Both parameters are inout, so the function swaps the caller’s variables
themselves. Without inout, it would swap its own copies and nothing would
change.
Put the lines in order
Put the lines in order. The method must be mutating; the program prints 3.
}count += 1mutating func increment() {counter.increment()print(counter.count)var counter = Counter()}for _ in 1...3 {var count = 0struct Counter {}
Show the correct program
struct Counter {
var count = 0
mutating func increment() {
count += 1
}
}
var counter = Counter()
for _ in 1...3 {
counter.increment()
}
print(counter.count)Mutation is still useful
None of this means mutation is bad. Building up a result in a var inside a
function, as in every lab so far, is mutation, and it is perfectly safe:
nothing outside the function can see the changes until the value is
returned. What value semantics removes is surprising mutation, a change
made in one place showing up in another.
Some things really should be shared: a bank account, a network connection, a game’s single world. For those, Swift has classes, the topic of the next lesson.
Lab 13: Mutation
Q1: Remove the odd numbers
Write removeOdd(_:), which removes every odd number from an array in
place.
func removeOdd(_ values: inout [Int]) {
// your code here
}
Show solution
func removeOdd(_ values: inout [Int]) {
values.removeAll { $0 % 2 != 0 }
}
var numbers = [1, 2, 3, 4, 5, -3]
removeOdd(&numbers)
assert(numbers == [2, 4])
var empty: [Int] = []
removeOdd(&empty)
assert(empty == [])removeAll(where:) removes every element for which the closure returns
true. Testing != 0 rather than == 1 handles negative odd numbers,
whose remainder is −1.
Q2: A bounded counter
Write a structure BoundedCounter with a limit set when it is created,
a read-only count starting at 0, a mutating increment() that stops at the
limit, and a mutating reset().
Show solution
struct BoundedCounter {
let limit: Int
private(set) var count = 0
init(limit: Int) {
self.limit = limit
}
mutating func increment() {
count = min(count + 1, limit)
}
mutating func reset() {
count = 0
}
}
var counter = BoundedCounter(limit: 2)
counter.increment()
counter.increment()
counter.increment()
assert(counter.count == 2)
var copy = counter
copy.reset()
assert(copy.count == 0)
assert(counter.count == 2) // the original is unaffectedThe last two assertions show value semantics: resetting copy does not
touch counter.
Q3: Rotate in place
Write rotateLeft(_:), which moves the first element of an array to the end,
in place. An empty array is left unchanged.
func rotateLeft(_ values: inout [Int]) {
// your code here
}
Show solution
func rotateLeft(_ values: inout [Int]) {
guard !values.isEmpty else {
return
}
let first = values.removeFirst()
values.append(first)
}
var numbers = [1, 2, 3, 4]
rotateLeft(&numbers)
assert(numbers == [2, 3, 4, 1])
rotateLeft(&numbers)
assert(numbers == [3, 4, 1, 2])
var none: [Int] = []
rotateLeft(&none)
assert(none == [])removeFirst() removes and returns the first element, and would stop the
program on an empty array, which is why the guard comes first.
What’s next
Next lesson: classes, the reference types used when something should be shared.