52

Reference vs. Value Types in Swift [FREE]

 5 years ago
source link: https://www.tuicool.com/articles/hit/NBrmYv7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Update note : Adam Rush updated this tutorial to iOS 12, Xcode 10, and Swift 4.2. Eric Cerney wrote the original tutorial.

If you’ve been keeping up with the sessions from recent WWDCs, you might have noticed a real emphasis on rethinking code architecture in Swift. One of the biggest differences developers note when coming to Swift from Objective-C is a heavy preference for value types over reference types .

In this tutorial, you’ll learn:

  • Key concepts of value and reference types
  • Differences between the two types
  • How to choose which to use

You’ll work through a real-world problem as you learn about the main concepts of each type. Additionally, you’ll learn more advanced concepts and discover some subtle, but important, points about both types.

Whether you’re coming from an Objective-C background, or you’re more versed in Swift, you’re sure to learn something about the ins and outs of typing in Swift.

Getting Started

First, create a new playground. In Xcode, select File ‣ New ‣ Playground… and name the playground ReferenceTypes .

Note : You can select any platform since this tutorial is platform-agnostic and only focuses on the Swift language aspects.

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

Reference Types vs. Value Types

So, what’s the core difference between these two types? The quick and dirty explanation is that reference types share a single copy of their data while value types keep a unique copy of their data.

Swift represents a reference type as a class . This is similar to Objective-C, where everything that inherits from NSObject is stored as a reference type.

There are many kinds of value types in Swift, such as struct , enum , and tuples. You might not realize that Objective-C also uses value types in number literals like NSInteger or even C structures like CGPoint .

To better understand the difference between the two, it’s best to start out with what you may recognize from Objective-C: reference types.

Reference Types

Reference types consist of shared instances that can be passed around and referenced by multiple variables. This is best illustrated with an example.

Add the following to your playground:

// Reference Types:

class Dog {
  var wasFed = false
}

The above class represents a pet dog and whether or not the dog has been fed. Create a new instance of your Dog class by adding the following:

let dog = Dog()

This simply points to a location in memory that stores dog . To add another object to hold a reference to the same dog, add the following:

let puppy = dog

Because dog is a reference to a memory address, puppy points to the exact same data in memory. Feed your pet by setting wasFed to true :

puppy.wasFed = true

puppy and dog both point to the exact same memory address.

eQrqaeq.png!web

Therefore you’d expect any change in one to be reflected in the other. Check that this is true by viewing the property values in your playground:

dog.wasFed     // true
puppy.wasFed   // true

Changing one named instance affects the other since they both reference the same object. This is exactly what you’d expect in Objective-C.

Value Types

Value types are referenced completely differently than reference types. You’ll explore this with some simple Swift primitives.

Add the following Int variable assignments and the corresponding operations to your playground:

// Value Types:

var a = 42
var b = a
b += 1

a    // 42
b    // 43

What would you expect a and b to equal? Clearly, a equals 42 and b equals 43 . If you’d declared them as reference types instead, both a and b would equal 43 since both would point to the same memory address.

The same holds true for any other value type. In your playground, implement the following Cat struct :

struct Cat {
  var wasFed = false
}

var cat = Cat()
var kitty = cat
kitty.wasFed = true

cat.wasFed        // false
kitty.wasFed      // true

This shows a subtle, but important, difference between reference and value types: Setting kitty ’s wasFed property has no effect on cat . The kitty variable received a copy of the value of cat instead of a reference .

value-value-480x206.png

Looks like your cat ’s going hungry tonight! :]

Although it’s much faster to assign a reference to a variable, copies are almost as cheap. Copy operations run in constant O(n) time since they use a fixed number of reference-counting operations based on the size of the data. Later on in this tutorial, you’ll see the clever methods in Swift that optimize these copy operations.

Mutability

var and let function differently for reference types and value types. Notice that you defined dog and puppy as constants with let , yet you were able to change the wasFed property. How’s that possible?

For reference types, let means the reference must remain constant. In other words, you can’t change the instance the constant references, but you can mutate the instance itself.

For value types, let means the instance must remain constant. No properties of the instance will ever change, regardless of whether the property is declared with let or var .

It’s much easier to control mutability with value types. To achieve the same immutability and mutability behaviors with reference types, you’d need to implement immutable and mutable class variants such as NSString and NSMutableString .

What Type Does Swift Favor?

It may surprise you that the Swift standard library uses value types almost exclusively. The results of a quick search through the Swift Standard Library for public instances of enum , struct , and class in Swift 1.2, 2.0, and 3.0 show a bias in the direction of value types:

Swift 1.2:

  • struct : 81
  • enum : 8
  • class : 3

Swift 2.0:

  • struct : 87
  • enum : 8
  • class : 4

Swift 3.0:

  • struct : 124
  • enum : 19
  • class : 3

This includes types like String , Array , and Dictionary , which are all implemented as struct s.

Which to Use and When

Now that you know the difference between the two types, when should you choose one over the other?

One situation leaves you no choice. Many Cocoa APIs require NSObject subclasses, which forces you into using class . Other than that, you can use the cases from Apple’s Swift blog under How to Choose? to decide whether to use a struct or enum value type or a class reference type. You’ll take a closer look at these cases in the following sections.

When to Use a Value Type

Here are three situations in which using a value type is the best choice.

Use a value type when comparing instance data with == makes sense .

I know what you’re thinking. Of course! You want every object to be comparable, right? But, you need to consider whether the data should be comparable. Consider the following implementation of a point:

struct Point: CustomStringConvertible {
  var x: Float
  var y: Float

  var description: String {
    return "{x: \(x), y: \(y)}"
  }
}

Does that mean two variables with the exact same x and y members are equal?

let point1 = Point(x: 2, y: 3)
let point2 = Point(x: 2, y: 3)

Yes. It’s clear that you should consider two Point s with the same internal values to be equal. The memory location of those values doesn’t matter. Your concern is the values themselves.

To make your Point comparable, you’d need to conform to the Equatable protocol, which is good practice for all value types. This protocol defines only one function which you must implement in order to compare two instances of the object.

This means that the == operator must have the following characteristics:

  • Reflexive : x == x is true
  • Symmetric : if x == y then y == x
  • Transitive : if x == y and y == z then x == z

Here’s a sample implementation of == for your Point :

extension Point: Equatable { }
func ==(lhs: Point, rhs: Point) -> Bool {
  return lhs.x == rhs.x && lhs.y == rhs.y
}

Use a value type when copies should have independent state .

Taking your Point example a little further, consider the following two Shape instances with their centers as two initially equivalent Point s:

struct Shape {
  var center: Point
}

let initialPoint = Point(x: 0, y: 0)
let circle = Shape(center: initialPoint)
var square = Shape(center: initialPoint)

What would happen if you altered the center point of one of the shapes?

square.center.x = 5   // {x: 5.0, y: 0.0}
circle.center         // {x: 0.0, y: 0.0}

Each Shape needs its own copy of a Point so you can maintain its state independent of the others. Can you imagine the chaos of all shapes sharing the same copy of a center Point ?

Use a value type when the code will use this data across multiple threads .

Value types allow you to get a unique, copied instance of data that you can trust no other part of your app (such as another thread) is changing. In a multi-threaded environment, this can be super useful and will prevent nasty bugs that are extremely hard to debug.

To make your data accessible from multiple threads and equal across threads, you’ll need to use a reference type and implement locking as well — no easy task!

If threads can uniquely own the data, using value types avoids potential conflict since each owner of the data holds a unique copy rather than a shared reference.

When to Use a Reference Type

Although value types are viable in a multitude of cases, reference types are still useful in many situations.

Use a reference type when comparing instance identity with === makes sense .

=== checks if two objects are exactly identical, right down to the memory address that stores the data.

To put this in real-world terms, consider the following: If your cubicle mate swaps one of your $20 bills with another legitimate $20 bill, you don’t really care, as you’re only concerned about the value of the object.

However, if someone stole the Magna Carta and created an identical parchment copy of the document in its place, that would matter greatly because the inherent identity of the document is not the same at all, and in this case, the identity matters.

You can use the same thought process when deciding whether to use reference types. There are very few times when you really care about the inherent identity — that is, the memory location — of the data. You usually just care about comparing the data values.

Use a reference type when you want to create a shared, mutable state .

Sometimes you want to store a piece of data as a single instance that multiple consumers can access and mutate.

A common example of an object with a shared, mutable state is a shared bank account. You might implement a basic representation of an account and person, the account holder, as follows:

class Account {
  var balance = 0.0
}

class Person {
  let account: Account

  init(_ account: Account) {
    self.account = account
  }
}

If any joint account holders add money to the account, all debit cards linked to the account should reflect the new balance:

let account = Account()

let person1 = Person(account)
let person2 = Person(account)

person2.account.balance += 100.0

person1.account.balance    // 100
person2.account.balance    // 100

Since Account is a class , each Person holds a reference to the account, and everything stays in sync.

Still Undecided?

If you’re not quite sure which mechanism applies to your situation, default to value types. You can always move to a class later with little effort.

swift_measure-320x320.png

Consider, though, that Swift uses value types almost exclusively, which is mind-boggling when you consider that the situation in Objective-C is completely the reverse.

As a coding architect under the new Swift paradigm, you need to do a bit of initial planning as to how your data will be used. You can solve almost any situation with either value types or reference types. However, using them incorrectly could result in a ton of bugs and confusing code.

In all cases, common sense and a willingness to change your architecture when new requirements come up is the best approach. Challenge yourself to follow the Swift model. You just might produce some nicer code than you expected!

You can download a completed version of this playground at the top or bottom of the tutorial by clicking on the Download Materials button.

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 an object.

To see some of these complications, below is 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, 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? :]

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:

  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.png

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!

jqIvmub.png!web

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK