Reference vs Value Types in Swift: Part 2/2

Eric Cerney
Value? Reference? Why not both in combination?

Value? Reference? Why not both in combination?

Welcome to the second and final part of our series on reference and value semantics in Swift! In Part 1, you explored the difference between reference and value types and the appropriate scenarios for each.

Part 2 uses a real-world example to demonstrate mixing value and reference types while maintaining expected semantics.


Update 11/5/16: This tutorial has been updated for Xcode 8 and Swift 3.

Getting Started

First, create a new playground: in Xcode, select File\New\Playground… and name the playground ValueSemanticsPart2. You can select either platform since this tutorial is platform-agnostic and only focuses on the Swift aspects.

Click Next, choose a convenient location to save the playground and click Create to open it.

Mixing Value and Reference Types

You’ll often run into situations where reference types need to contain value types, and vice versa. This can easily complicate the expected semantics of the object.

To see some of these complications, you’ll look at an example of each scenario.

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, that stores an Address structure where equality matters.

To see how this may 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
}

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 the two addresses were 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 straight forward 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 the billedTo Person object will be shared by numerous Bill instances. 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:

// Do not add this 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 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 obviously created Bill as a struct for a reason, and making it rely on a shared instance defeats the original purpose. 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:

  1. First, you create a new Person based on an Address and name.
  2. Next, you instantiate a new Bill using the default initializer and create a copy by assigning it to a new constant.
  3. Finally, you mutate the passed-in Person object, which in turn…affects the supposedly unique instances.

Hmm, 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 “Bill”, and the other to still be “Robert”.

What you could do is 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

Replace your implementation of Bill with the following:

struct Bill {
  let amount: Float
  let billedTo: Person
 
  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 was an explicit initializer. Instead of simply assigning billedTo, you create a new Person instance with the same data (name and address) as the one passed in. The caller won’t be able to edit their original copy of Person and affect Bill.

Look at the two printout lines at the bottom of your playground where you check the value of each instance of Bill. You’ll see that each kept their original values 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 it could be mutated by an outside entity 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; they’ve been mutated by some outside entity — that is, 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 could 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. First, you created a private variable to hold a reference to the Person object.
  2. Next, you created a computed property to return the private variable for read operations.
  3. Finally, you created a computed property 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 meant, this approach would solve your issue. In a perfect world, your caller would always use billedToForRead to get data from your reference and billedToForWrite to make a change to the reference.

value-apirage

But that’s not how the world works, does it? :]

Defensive Mutating Methods

You’ll have to add a bit of defensive code here. To solve this, 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:

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

Declaring this method as mutating means you can only call it 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 single time you write to it. A better way is to only copy the data if multiple objects hold 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 mimics what Swift itself does when working with value types.

To see this in action, add the following logging to billedToForWrite:

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
  }
}

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 from the results sidebar to display the Person object.

Now add the following below myBill 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 a new copy isn’t made. Sweet :]

There you have it, efficient value semantics while combining reference and value types!

Where to Go From Here?

You can download the complete playground with all the code in this series 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 lazily copying the data only when needed and how to circumvent the confusing state 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 like the one above. If you find yourself in this scenario, it’s a good sign something needs a bit of redesign.

The example in this tutorial was bent on ensuring a Bill could hold reference to a Person, but perhaps 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 series; you can use what you’ve learned here to modify the way you approach value types in your code and hopefully avoid confusing and obfuscated code.

If you have any comments or questions feel free to join in the forum discussion below!

Eric Cerney

Eric is an iOS Software Engineer in San Francisco. After being acquired by Capital One, he likes to spend his days at work hanging out with Samuel L. Jackson and asking everyone "What's in your wallet?". Lately his main focus has been with Swift and gaining a deeper knowledge of programming languages at the core.

Outside iOS, his interests are tinkering with hardware (Raspberry Pi and Arduino), gaming, exploring San Francisco, and regretting endless Netflix marathons. You can find Eric on Twitter or his personal site.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 32 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!