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?
Defensive Mutating Methods
You’ll have to add a bit of defensive code here. To solve this problem, you could hide the two new properties from the outside and create methods to interact with them properly.
Replace your implementation of Bill
with the following:
struct Bill {
let amount: Float
private var _billedTo: Person
// 1
private var billedToForRead: Person {
return _billedTo
}
private 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)
}
// 2
mutating func updateBilledToAddress(address: Address) {
billedToForWrite.address = address
}
mutating func updateBilledToName(name: String) {
billedToForWrite.name = name
}
// ... Methods to read billedToForRead data
}
Here’s what you changed above:
Declaring these methods as mutating
means you can only call them when you instantiate your Bill
object using var
instead of let
. This behavior is exactly what you’d expect when working with value semantics.
-
You made both computed properties
private
so that callers can’t access the properties directly. -
You added
updateBilledToAddress
andupdateBilledToName
to mutate thePerson
reference with a new address or name. This approach makes it impossible for someone else to incorrectly updatebilledTo
, since you’re hiding the underlying property.Declaring these methods as
mutating
means you can only call them when you instantiate yourBill
object usingvar
instead oflet
. This behavior is exactly what you’d expect when working with value semantics.
A More Efficient Copy-on-Write
The last thing to do is improve the efficiency of your code. You currently copy the reference type Person
every time you write to it. A better way is to only copy the data if multiple objects hold a reference to it.
Replace the implementation of billedToForWrite
with the following:
private var billedToForWrite: Person {
mutating get {
if !isKnownUniquelyReferenced(&_billedTo) {
_billedTo = Person(name: _billedTo.name, address: _billedTo.address)
}
return _billedTo
}
}
isKnownUniquelyReferenced(_:)
checks that no other object holds a reference to the passed-in parameter. If no other object shares the reference, then there’s no need to make a copy and you return the current reference. That will save you a copy, and it mimics what Swift itself does when working with value types.
To see this in action, modify billedToForWrite
to match the following:
private var billedToForWrite: Person {
mutating get {
if !isKnownUniquelyReferenced(&_billedTo) {
print("Making a copy of _billedTo")
_billedTo = Person(name: _billedTo.name, address: _billedTo.address)
} else {
print("Not making a copy of _billedTo")
}
return _billedTo
}
}
Here you’ve just added logging so you can see when a copy is or isn’t made.
At the bottom of your playground, add the following Bill
object to test with:
var myBill = Bill(amount: 99.99, billedTo: billPayer)
Next, update the bill using updateBilledToName(_:)
by adding the following to the end of your playground:
myBill.updateBilledToName(name: "Eric") // Not making a copy of _billedTo
Because myBill
is uniquely referenced, no copy will be made. You can verify this by looking in the debug area:
billedToForWrite
from updateBilledToName(_:)
and another access from the results sidebar to display the Person
object.
Now add the following below the definition of myBill
and above the call to updateBilledToName
to trigger a copy:
var billCopy = myBill
You’ll now see in the debugger that myBill
is actually making a copy of _billedTo
before mutating its value!
You’ll see an extra print for the playground’s results sidebar, but this time it won’t match. That’s because updateBilledToName(_:)
created a unique copy before mutating its value. When the playground accesses this property again, there’s now no other object sharing reference to the copy, so it won’t make a new copy. Sweet. :]
There you have it: efficient value semantics and combining reference and value types!
You can download a completed version of this playground at the top or bottom of the tutorial by clicking on the Download Materials button.
Where to Go From Here?
In this tutorial, you learned that both value and reference types have some very specific functionality which you can leverage to make your code work in a predictable manner. You also learned how copy-on-write keeps value types performant by copying the data only when needed. Finally, you learned how to circumvent the confusion of combining value and reference types in one object.
Hopefully, this exercise in mixing value and reference types has shown you how challenging it can be to keep your semantics consistent, even in a simple scenario. If you find yourself in this scenario, it’s a good sign something needs a bit of redesign.
The example in this tutorial focused on ensuring a Bill
could hold reference to a Person
, but you could have used a Person
’s unique ID or simply name
. To take it a step further, maybe the whole design of a Person
as a class
was wrong from the outset! These are the types of things you have to evaluate as your project requirements change.
I hope you enjoyed this tutorial. You can use what you’ve learned here to modify the way you approach value types and avoid confusing code.
If you have any comments or questions, please join the forum discussion below!