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 4 of 4 of this article. Click here to view the first page.

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.

  1. You made both computed properties private so that callers can’t access the properties directly.
  2. You added updateBilledToAddress and updateBilledToName to mutate the Person reference with a new address or name. This approach makes it impossible for someone else to incorrectly update billedTo, since you’re hiding the underlying property.

    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.

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:

NoCopyDebugger

Note: You’ll actually see the print result twice. This is because the playground’s results sidebar dynamically resolves the object on each line to give you a preview. This results in one access to 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!

CopyDebugger

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!