Swift 5: Value Vs Reference types

In this article, we’re going to discuss Swift 5 value and reference types and see how they’re handled.

Value types are faster to work with since they operate out of the stacks, and copying a value is cheap since it happens in a constant time. Values also help us achieve predictable behaviour and isolation, whereas reference types give shared access to memory locations and dynamic storage.

Given the above-mentioned reason, references can cause surprising behaviours you might not want as a developer.

Let’s dive a bit deeper into some of the differences between these two types.

Introduction

Swift only has a total of six types: These consist of the four Named types or Nominal types:

  • protocol: Value type (but can be reference types)
  • struct: Value type
  • enum: Value type
  • class: Reference type

And two Unnamed/Compound types:

  • tuple: Value type
  • function: Reference type

Structs, enums and tuples have value semantics, whereas classes and functions have reference semantics. We can combine value and reference semantics together to give us the best of both worlds.

Difference between value types and reference types

In this section, we’ll demonstrate the differences between value types and reference types, and then discuss an example exploring reference types in order to make a testable design.

In the first example, we have TestValue as a value and TestReference as a reference type, as follows:

struct TestValue {
    var temp1, temp2 : Int
}

final class TestReference {
var temp1, temp2 : Int

init(temp1: Int, temp2: Int) {
    self.temp1 = temp1
    self.temp2 = temp2
  }
}

This creates a value and reference version of the Test. Let’s now look at the behaviour of the value type:

do {
    let testValue1 = TestValue(temp1: 1, temp2: 2)
    var testValue2 = testValue1
    testValue2.temp1 = 10
    dump(testValue1)
    dump(testValue2)
}

Next, let’s look at the behaviour of the reference type:

do {
    let testReference1 = TestReference(temp1: 1, temp2: 2)
    let testReference2 = testReference1
    testReference2.temp1 = 10
    dump(testReference1)
    dump(testReference2)
}

As we mentioned earlier, reference types work on the same memory addresses—as such, changing one client reflects a change in the other client. In large complicated programs, this interaction can result in confusing behaviour.

It’s because of this reason that managing the state of the program becomes hard when we work extensively with reference types. However, this behaviour can be used for good. For example, we can use it to implement “action at a distance testing”.

Use of Reference Types in Testing Value types

Let’s have a look at the following struct:

import Foundation

struct Counter {

    private var start: Double?
    private var totalCount: Double = 0
    private var incrementBy: Double

    var isCounterRunning: Bool {
        return start != nil
    }

    var incrementedCount: Double {
        return totalCount + (start.map{ _ in incrementBy} ?? 0)
    }

    mutating func startCounter() {
        start = 1
    }

    mutating func pauseCounter() {
        totalCount = incrementedCount
        start = nil
    }

    mutating func resetCounter() {
        start = nil
        totalCount = 0
    }
}

You can see that it’s really hard to test the functionality in this struct. Now let’s try to fix the issues in the above code by adding a mock to make it more testable.

First, we’ll add a protocol called CountKeeper and then a struct RealTimeCounter to provide default implementation to the protocol, as follows:

protocol CountKeeper {
    var count: Double { get }
}

struct RealTimeCounter: CountKeeper {
    var count: Double {
        return 1
    }
}

Now we can go ahead an inject this CountKeeper protocol dependency to our Counter to make it more testable:

struct Counter {

    internal var countKeeper: CountKeeper

    init(countKeeper: CountKeeper = RealTimeCounter()) {
        self.countKeeper = countKeeper
    }

    private var start: Double?
    private var totalCount: Double = 0

    var isCounterRunning: Bool {
        return start != nil
    }

    var incrementedCount: Double {
        return totalCount + (start.map {_ in countKeeper.count } ?? 0)
    }

    mutating func startCounter() {
        start = countKeeper.count
    }

    mutating func pauseCounter() {
        totalCount = incrementedCount
        start = nil
    }

    mutating func resetCounter() {
        start = nil
        totalCount = 0
    }
}

Finally, let’s create a mock object that’s a reference type:

final class TestCounter: CountKeeper {

    var count : Double = 1
}

Now we can write a number of tests by making use of this reference type mock object, as this mock object lets us reach inside the Test class and change it from the outside. Simultaneously, we can rest assured that such a behaviour will not be exhibited in our production code since the CountKeeper for production code is a struct. You can see the result of multiple test cases in the screenshot below:

For other updates you can follow me on Twitter on my twitter handle @NavRudraSambyal

Thanks for reading, please share it if you found it useful 🙂

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *