00:00 Let's talk about structs and mutation today. 00:12 We'll
discuss two topics: how mutation on structs differs from mutation on objects,
and how can we achieve mutation on structs. 00:22 Mutation with objects
sounds a bit dangerous; if you have objects and you mutate them somewhere in
your app (for example, on a different queue), they can change other parts of
your app as well. 00:44 If you have one object and many variables
pointing to it, every value of every variable will change. 00:51 You can
even run into race conditions, so you have to be very careful when mutating
objects. Yet it's very useful, because often you want sharing — for example,
with a UIScreen.
Mutation
01:05 The functional programming world said: "We think mutation is bad,
so we'll make everything immutable," which is a very different approach. Structs
in Swift hit the sweet spot, because you can mutate a struct, but you don't have
global side effects. 01:22 Mutating structs is very different from
mutating objects, in that mutating a struct only changes a single variable and
not all variables with the same value. 01:34 As an example, here we have
an array, x
. If we want to sort it, we can just call x.sort()
, which will
mutate the array in place:
var x = [3, 1, 2]
x.sort()
01:50 Because it's a mutating method, we need to define x
as var
.
Otherwise, we can't even call sort
. After calling sort
, x
has a new value.
A mutating method on a struct only changes a single variable; if we would've
created a copy of x
, then calling the mutating method on x
wouldn't have
changed the copy:
var x = [3, 1, 2]
let y = x
x.sort()
x y
02:26 The value of x
changes within its scope — for example, it
changes within a function body — but it doesn't change anything outside of its
scope. We could also make the code a bit more elaborate and loop over
x.indices
and square each value within the loop's body. Even though we're
mutating x
, we don't change y
:
var x = [3, 1, 2]
let y = x
x.sort()
for idx in x.indices {
x[idx] *= x[idx]
}
y
An Immutable sort
03:16 There's another approach to solve the same problem (sorting and
squaring). We can use a different version of sort
, called sorted
. This
method doesn't mutate the array in place, but it returns a new, sorted array. We
can verify that y
is unchanged, but the return value is now sorted. We can
easily chain these non-mutating methods together and continue to do
calculations. 04:13 We can square using map
and keep on composing.
Composing things by chaining method calls is very different from writing the
mutating version:
y.sorted().map { $0 * $0 }
04:24 In the end, both versions are equivalent, so it's mostly a matter
of taste: which version makes your code more readable? It's hard to say that one
is better than the other. It depends on what you're doing, and then you can
decide on the nicest API. 04:54 In this case, the immutable variant is
more readable because it's more compact. If we wanted to implement an in-place
quicksort, however, the mutable version would be more readable (and possibly the
only way to implement the quicksort).
05:08 Let's look at writing mutating methods. We'll start with an
Account
struct, which has a balance
property. We'll add a deposit
method,
which has an amount
parameter. The deposit
method returns a new, updated
Account
value:
struct Account {
let balance: Int
func deposit(amount: Int) -> Account {
return Account(balance: balance + amount)
}
}
05:57 This deposit
method is similar to sorted
because it doesn't
change the original value, but instead returns a new value. We'll change the
name to depositing
to reflect that. Now we can use it:
struct Account {
let balance: Int
func depositing(amount: Int) -> Account {
return Account(balance: balance + amount)
}
}
let account = Account(balance: 0)
account.depositing(amount: 100)
A mutating
Variant
06:29 The above is one way of implementing depositing
. Functional
programmers like this style: just keep returning new values. A different way
would be to create a mutating
variant. We can just add a new method,
deposit
, that's marked as mutating
. Because it's mutating
, it doesn't need
to return a new value:
struct Account {
var balance: Int
func depositing(amount: Int) -> Account {
return Account(balance: balance + amount)
}
mutating func deposit(amount: Int) {
balance += amount
}
}
07:19 To make this work, we also need to change the property declaration
of balance
to a var
, because if we declare it as a let
, we can never
change it again. Writing the property as var
, rather than let
, might feel a
bit impure, but we can still control mutability through the variable that points
to the account. For example, we can't call account.deposit
:
let account = Account(balance: 0)
account.deposit(amount: 100)
07:57 The compiler will give us an error, because we can't call a
mutating method on a variable that's declared with let
; we have to change it
to var
. Now, let's have a look at the result of account.balance
:
var account = Account(balance: 0)
account.depositing(amount: 100)
account.deposit(amount: 10)
account.balance
08:26 The call to depositing()
doesn't change the variable, because it
returns a new value.
08:42 The deposit
method changes the value of the variable. If we
would've created a different account, then calling deposit
on one variable
wouldn't change the other variable. We can say that a mutating method on a
struct is safer than a method that changes an object. Because it doesn't have
these global side effects, it only changes a single variable.
Equivalence
09:15 We can see that both approaches are equivalent: we could write the
mutating version in terms of the non-mutating version, and vice versa. For
example:
struct Account {
var balance: Int
func depositing(amount: Int) -> Account {
return Account(balance: balance + amount)
}
mutating func deposit(amount: Int) {
self = depositing(amount: amount)
}
}
10:03 And here it is the other way around:
struct Account {
var balance: Int
func depositing(amount: Int) -> Account {
var copy = self
copy.deposit(amount: amount)
return copy
}
mutating func deposit(amount: Int) {
balance += amount
}
}
10:29 We can call the mutating method because copy
is declared as a
var
. Because Account
is a struct, it actually makes a copy and copy
is now
an independent variable.
10:41 In a way, both methods do the same thing. They behave slightly
differently, but once you have one, you can always declare the other.
The inout
Keyword
10:52 In addition, there's another related keyword. mutating
works on
methods, but we can use inout
for function parameters. This also sounds a bit
dangerous, because it might remind you of passing a reference, but mutating
and inout
are actually the same thing.
11:14 We can write deposit
in a free function and pass in the account
as an inout
parameter. We can then freely mutate it within the body of the
function:
func deposit(amount: Int, into account: inout Account) {
account.balance += amount
}
12:01 inout
is basically the same thing as mutating
is for self
:
it allows you to mutate the value that you get passed in.
12:16 We can call our new function, and because the parameter is
inout
, we need to prefix it with an ampersand. It looks like we're dealing
with pointers, but it's different. When you declare an inout
parameter, the
value gets copied into the function. Within the function we can change it, and
then it gets copied back out when the function is done. It's not a mutable
pointer, because within the function, you're working with your own, independent
copy:
var account = Account(balance: 0)
deposit(amount: 10, into: &account)
13:32 We can also use inout
and mutating
at the same time. For
example, if we want to transfer money from one account into another, we can
write a mutating method, which takes an inout
parameter as well:
struct Account {
var balance: Int
mutating func transfer(amount: Int, from: inout Account) {
balance += amount
from.balance -= amount
}
}
14:19 It's also easy to see that inout
means copy-in copy-out. You
can't dispatch to another thread and change an inout
parameter (because that
would let the inout
parameter escape).
14:53 Depending on what problem you're solving, you can choose your
strategy. You can write immutable methods, you can write mutating methods, or
you can use inout
parameters. They're all equivalent. There might be small
performance differences, but for most code, it doesn't matter. Generally, it's
best to decide based on readability and choose the version that makes your code
the clearest at the call site.