0:00 Let's talk about structs and mutation today. We'll discuss two
topics: how mutation on structs differs from mutation on objects, and how can we
achieve mutation on structs. 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. If you have
one object and many variables pointing to it, every value of every variable will
change. 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.
1: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. 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. As an example, here we have an array, x, and if we want
to sort it, we can just call x.sort(), and this will mutate the array in
1: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 change
2: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:
3: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. We can square using map and keep on composing. Composing things
by chaining method calls is very different from writing the mutating version:
4: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. 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).
5: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
6: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:
7: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:
7: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:
8:26 The call to depositing() doesn't change the variable, because it
returns a new value.
8: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.
9: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
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
12:01inout is basically the same thing as what 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
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:
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. Most of the time,
it's best to decide based on readability, and choose the version that makes your
code the clearest at the call site.