Reference vs. Value Types in Swift
Learn the subtle, but important, differences between reference and value types in Swift by working through a real-world problem. By Adam Rush.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Reference vs. Value Types in Swift
30 mins
- Getting Started
- Reference Types vs. Value Types
- Reference Types
- Value Types
- Mutability
- What Type Does Swift Favor?
- Which to Use and When
- When to Use a Value Type
- When to Use a Reference Type
- Still Undecided?
- Mixing Value and Reference Types
- Reference Types Containing Value Type Properties
- Value Types Containing Reference Type Properties
- Getting Value Semantics From Mixed Types
- Copying References During Initialization
- Using Copy-on-Write Computed Properties
- Defensive Mutating Methods
- A More Efficient Copy-on-Write
- Where to Go From Here?
Reference Types Containing Value Type Properties
It’s quite common for a reference type to contain value types. An example would be a Person
class where identity matters, which stores an Address
structure where equality matters.
To see how this might look, replace the contents of your playground with the following basic implementation of an address:
struct Address {
var streetAddress: String
var city: String
var state: String
var postalCode: String
}
In this example, all properties of Address
together form a unique physical address of a building in the real world. The properties are all value types represented by String
; the validation logic has been left out to keep things simple.
Next, add the following code to the bottom of your playground:
class Person { // Reference type
var name: String // Value type
var address: Address // Value type
init(name: String, address: Address) {
self.name = name
self.address = address
}
}
This mixing of types makes perfect sense in this scenario. Each class instance has its own value type property instances that aren’t shared. There’s no risk of two different people sharing and unexpectedly changing the address of the other person.
To verify this behavior, add the following to the end of your playground:
// 1
let kingsLanding = Address(
streetAddress: "1 King Way",
city: "Kings Landing",
state: "Westeros",
postalCode: "12345")
let madKing = Person(name: "Aerys", address: kingsLanding)
let kingSlayer = Person(name: "Jaime", address: kingsLanding)
// 2
kingSlayer.address.streetAddress = "1 King Way Apt. 1"
// 3
madKing.address.streetAddress // 1 King Way
kingSlayer.address.streetAddress // 1 King Way Apt. 1
Here’s what you added:
- First, you created two new
Person
objects from the sameAddress
instance. - Next, you changed the address of one person.
- Last, you verified that the two addresses are different. Even though each object was created using the same address, changing one doesn’t affect the other.
Where things get messy is when a value type contains a reference type, as you’ll explore next.
Value Types Containing Reference Type Properties
Things were so straightforward in the previous example. How could the opposite scenario be so much more difficult?
Add the following code to your playground to demonstrate a value type containing a reference type:
struct Bill {
let amount: Float
let billedTo: Person
}
Each copy of Bill
is a unique copy of the data, but numerous Bill
instances will share the the billedTo
Person
object. This adds quite a bit of complexity in maintaining the value semantics of your objects. For instance, how do you compare two Bill
objects since value types should be Equatable
?
You could try the following (but don’t add it to your playground!):
extension Bill: Equatable { }
func ==(lhs: Bill, rhs: Bill) -> Bool {
return lhs.amount == rhs.amount && lhs.billedTo === rhs.billedTo
}
Using the identity operator ===
checks that the two objects have the exact same reference, which means the two value types share data. That’s exactly what you don’t want when following value semantics.
So what can you do?
Getting Value Semantics From Mixed Types
You created Bill
as a struct
for a reason and making it rely on a shared instance means your struct isn’t an entirely unique copy. That defeats much of the purpose of a value type!
To get a better understanding of the issue, add the following code to the bottom of your playground:
// 1
let billPayer = Person(name: "Robert", address: kingsLanding)
// 2
let bill = Bill(amount: 42.99, billedTo: billPayer)
let bill2 = bill
// 3
billPayer.name = "Bob"
// Inspect values
bill.billedTo.name // "Bob"
bill2.billedTo.name // "Bob"
Taking each numbered comment in turn, here’s what you did:
-
First, you created a new
Person
based on anAddress
and name. -
Next, you instantiated a new
Bill
using the default initializer and created a copy by assigning it to a new constant. -
Finally, you mutated the passed-in
Person
object, which in turn affected the supposedly unique instances.
Oh, no! That’s not what you want. Changing the person in one bill changes the other. Because of value semantics, you’d expect one to be Bob and the other to be Robert.
Here, you could make Bill
copy a new unique reference in init(amount:billedTo:)
. You’ll have to write your own copy
method, though, since Person
isn’t an NSObject
and doesn’t have its own version.
Copying References During Initialization
Add the following at the bottom of your implementation of Bill
:
init(amount: Float, billedTo: Person) {
self.amount = amount
// Create a new Person reference from the parameter
self.billedTo = Person(name: billedTo.name, address: billedTo.address)
}
All you added here is an explicit initializer. Instead of simply assigning billedTo
, you create a new Person
instance using the passed-in name and address. As a result, the caller won’t be able to affect Bill
by editing their original copy of Person
.
Look at the two printout lines at the bottom of your playground and check the value of each instance of Bill
. You’ll see that each kept its original value even after mutating the passed-in parameter:
bill.billedTo.name // "Robert"
bill2.billedTo.name // "Robert"
One big issue with this design is that you can access billedTo
from outside the struct. That means an outside entity could mutate it in an unexpected manner.
Add the following to the bottom of the playground, just above the printout lines:
bill.billedTo.name = "Bob"
Check the printout values now. You should see that an outside entity has mutated them — it’s your rogue code above.
bill.billedTo.name = "Bob"
// Inspect values
bill.billedTo.name // "Bob"
bill2.billedTo.name // "Bob"
The issue here is that even if your struct is immutable, anyone with access to it can mutate its underlying data.
Using Copy-on-Write Computed Properties
Native Swift value types implement an awesome feature called copy-on-write. When assigned, each reference points to the same memory address. It’s only when one of the references modifies the underlying data that Swift actually copies the original instance and makes the modification.
You could apply this technique by making billedTo
private and only returning a copy on write.
Remove the test lines at the end of the playground:
// Remove these lines:
/*
bill.billedTo.name = "Bob"
bill.billedTo.name
bill2.billedTo.name
*/
Now, replace your current implementation of Bill
with the following code:
struct Bill {
let amount: Float
private var _billedTo: Person // 1
// 2
var billedToForRead: Person {
return _billedTo
}
// 3
var billedToForWrite: Person {
mutating get {
_billedTo = Person(name: _billedTo.name, address: _billedTo.address)
return _billedTo
}
}
init(amount: Float, billedTo: Person) {
self.amount = amount
_billedTo = Person(name: billedTo.name, address: billedTo.address)
}
}
Here’s what’s going on with this new implementation:
-
You created a private variable
_billedTo
to hold a reference to thePerson
object. -
Next, you created a computed property
billedToForRead
to return the private variable for read operations. -
Finally, you created a computed property
billedToForWrite
which will always make a new, unique copy ofPerson
for write operations. Note that this property must also be declared as mutating, since it changes the underlying value of the structure.
If you can guarantee that your caller will use your structure exactly as you intend, this approach will solve your problem. In a perfect world, your caller will always use billedToForRead
to get data from your reference and billedToForWrite
to make a change to the reference.
But that’s not how the world works, is it? :]