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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. First, you created two new Person objects from the same Address instance.
  2. Next, you changed the address of one person.
  3. 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:

  1. First, you created a new Person based on an Address and name.
  2. Next, you instantiated a new Bill using the default initializer and created a copy by assigning it to a new constant.
  3. 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:

  1. You created a private variable _billedTo to hold a reference to the Person object.
  2. Next, you created a computed property billedToForRead to return the private variable for read operations.
  3. Finally, you created a computed property billedToForWrite which will always make a new, unique copy of Person 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? :]