Swift: Value Types vs Reference Types, and When to Use Each

In Swift there are two categories of types: value types and reference types. A value type instance keeps a unique copy of its data, for example, a struct or an enum. A reference type, shares a single copy of its data, and the type is usually a class.

We’ll also discuss types like tuples, closures, and functions, how they work, how to use them, and what happens when you mix types.

You’ll use Xcode 10.1 and Swift 4.2 in this tutorial.

First, create a new Playground in Xcode. You'll use it to experiment with the code in this tutorial.

Value Types vs Reference Types

Value Types

A value type instance is an independent instance and holds its data in its own memory allocation. There are a few different value types: struct, enum, and tuple.

Struct

Let’s experiment with structs and prove that they’re value types:

Add the following code to your playground:

// 1
struct Car {
    let brand: String
    var model: String
}

// 2
var golf = Car(brand: "Volkswagen", model: "Golf")
// 3
let polo = golf

// 4
golf.model = "Golf 2019"

// 5
print(golf)
print(polo)

In the code above, you will:

  1. Create a Car struct with brand and model properties.
  2. Create a new instance of Car named golf.
  3. Create a copy of the golf instance, named polo.
  4. Change the golf.model variable to Golf 2019
  5. Print the 2 different instances. The first print statement prints Car(brand: "Volkswagen", model: "Golf 2019") in the Console. The second one prints Car(brand: "Volkswagen", model: "Golf"). Even if polo is a copy of golf, the instances remain independent with their own unique data copies.

With this simple playground, we’ve confirmed that structs are indeed value types.

Enum

To check that enums are value types, add this code to the playground:

// 1
enum Language {
    case italian
    case english
}

// 2
var italian = Language.italian
// 3
let english = italian

// 4
italian = .english

// 5
print(italian)
print(english)

In the code above, you will:

  1. Create a Language enum with italian and english cases.
  2. Create a new instance of Language for the italian language.
  3. Create a copy of the italian instance, named english.
  4. Change the italian instance to english.
  5. Print the two different instances. The first print statement prints english, and the second one prints italian. Even if english is a copy of italian, the instances remain independent.

Tuple

The last value type that we'll explore is tuple. A tuple type is a comma-separated list of zero or more types, enclosed in parentheses. You can access its values using the dot (.) notation followed by the index of the value.

You can also name the elements in a tuple and use the names to access the different values.

Add the following code to the playground:

// 1
var ironMan = ("Tony", "Stark")
// 2
let parent = ironMan

// 3
ironMan.0 = "Alfred"

// 4
print(ironMan)
print(parent)

In the code above, you will:

  1. Create an ironMan tuple with the strings Tony and Stark.
  2. Create a copy of the ironMan instance, named parent.
  3. Change the ironMan.0 index to Alfred.
  4. Print the 2 different instances. The first print, prints ("Alfred", "Stark") and the second one, prints ("Tony", "Stark"). Again, the instances remain independent.

You can now be certain that structs, enums, and tuples are value types! 🎉

Learning value types with Iron ManSource: Marvel Entertainment

When to Use Value Types

Use value types when comparing instance data with == makes sense.
== checks if every property of the two instances is the same.
With value types you always get a unique, copied instance, and you can be sure that no other part of your app is changing the data under the hood. This is especially helpful in multi-threaded environments where a different thread could alter your data.

Use a value type when you want copies to have an independent state, and the data will be used in code across multiple threads.

In Swift, Array, String, and Dictionary are all value types.

Reference Types

In Swift, reference type instances share a single copy of their data, so that every new instance will point to the same address in memory. A typical example is a class, function, or closure.

To explore these, add a new function to your playground:

func address<T: AnyObject>(of object: T) -> Int {
    return unsafeBitCast(object, to: Int.self)
}

This function prints the address of an object, which will help you check whether you're referencing the same instance or not.

Class

The first reference type that you'll look at is a class.

Add the following code to your playground:

// 1
class Dog: CustomStringConvertible {
    var age: Int
    var weight: Int

    // 2
    var description: String {
        return "Age \(age) - Weight \(weight)"
    }

    // 3
    init(age: Int, weight: Int) {
        self.age = age
        self.weight = weight
    }
}

// 4
let doberman = Dog(age: 1, weight: 70)
// 5
let chihuahua = doberman

// 6
doberman.age = 2
// 7
chihuahua.weight = 10

// 8
print(doberman)
print(chihuahua)

// 9
print(address(of: doberman))
print(address(of: chihuahua))

In the code above, you will:

  1. Create a new class named Dog, that conforms to CustomStringConvertible to print the custom descriptions of the object.
  2. Define the custom description of the object.
  3. Create a new init function. This is needed because, unlike a struct, a class doesn't automatically create an initialization function based on the variables of the object.
  4. Create a doberman instance of Dog.
  5. Create a copy of doberman, named chihuahua.
  6. Change the doberman.age to 2.
  7. Change the chihuahua.weight to 10.
  8. Print the description of the two different instances. The first print, prints Age 2 - Weight 10, and the second one prints the same; Age 2 - Weight 10. This is because you're actually referencing the same object.
  9. Print the address of the two different instances. With these prints, you'll be sure that you're referencing the same address. You'll see that both print statements print the same value.

You can rest assured that a class is a reference type.

Functions and Closures

A closure is used to refer to a function along with the variables from its scope that it encapsulates. Functions are essentially closures that store references to variables in their context.

Take a look at the code below:

let closure = { print("Test") }
func function() -> (){ print("Test") }

closure()
function()

They both do the same thing.

You can find more info about closures in Swift's docs.

When to Use Reference Types

Use a reference type when comparing instance identity with === makes sense. === checks if two objects share the same memory address.

They’re also useful when you want to create a shared, mutable state.

As a general rule, start by creating your instance as an enum, then move to a struct if you need more customization, and finally move to class when needed.

Mutability

The mutability of value types lets you specifically choose what variables can be modified or not.

Add this code to your playground:

// 1
struct Bike {
    // 2
    let radius: Int
    var km: Float
}

// 3
let bike = Bike(radius: 22, km: 34.5)

In this code, you will:

  1. Create a Bike struct.
  2. Create a let and a var property.
  3. Create a Bike constant. Even if km inside the Bike struct is a var, you can't edit that because bike is a let.

If you create a struct constant, you can’t change the values of its properties, even though the values themselves might be variables.

A class, on the other hand, lets you change them because you are referencing the memory address of the object.

Add the following code:

// 1
class Motorbike {
    // 2
    let radius: Int
    var km: Float

    init(radius: Int, km: Float) {
        self.radius = radius
        self.km = km
    }
}

// 3
let motorbike = Motorbike(radius: 22, km: 34.5)
motorbike.km += 1

// 4
print(motorbike.km)

In the code above, you will:

  1. Create a Motorbike class.
  2. Create radius as let and km as var.
  3. Add 1 to the motorbike.km variable.
  4. Print the motorbike.km variable.

You can set the km variable because Motorbike is a class and we are referencing its memory address.

Dark Knight on a motorocycleSource: Warner Bros. Pictures

Nonmutating Keyword

There is also a nonmutating keyword, which can specify that a constant can be set without modifying the containing instance, but instead has global side effects.

Again, add this code to the playground:

// 1
import Foundation

// 2
struct Cat: CustomStringConvertible {
    // 3
    var name: String? {
        // 4
        get {
            return UserDefaults.standard.string(forKey: "CatName")
        }

        // 5
        nonmutating set {
            if let newValue = newValue {
                UserDefaults.standard.set(newValue, forKey: "CatName")
            } else {
                UserDefaults.standard.removeObject(forKey: "CatName")
            }
        }
    }

    var description: String {
        return name ?? ""
    }
}

// 6
let cat = Cat()
cat.name = "Sam"

// 7
let tiger = Cat()
tiger.name = "Olly"

In the code above, you:

  1. Import Foundation because playgrounds don't do that by default.
  2. Create the Cat struct.
  3. Create the name variable.
  4. Specify how to get the Cat name, by accessing the UserDefaults.
  5. Specify the nonmutating set behavior to set the new name in UserDefaults if not nil, otherwise remove it.
  6. Create a new Cat instance, named cat, and set its name to "Sam".
  7. Create a new Cat instance, named tiger, and set its name to "Olly".

So, what’s going on here? Both the cats have "Olly" as their name because they’re using UserDefaults to get their name. Also, even if both are constants, you can set their name property without any warning or error.

Mixed Types

What happens when you mix value types with reference types and vice versa?

You may often run into complications by mixing these types together. Let’s take a look at some examples so that you can avoid these common pitfalls.

Mixing Value Types in Reference Types

Let’s start by mixing value types in reference types by adding a struct inside a class and see what happens.

Let's say that you have a manufacturer that produces some device. You'll create a class Device and a struct Manufacturer.

Add the following code to the playground:

// 1
struct Manufacturer {
    var name: String
}

// 2
class Device {
    var name: String
    // 3
    var manufacturer: Manufacturer

    init(name: String, manufacturer: Manufacturer) {
        self.name = name
        self.manufacturer = manufacturer
    }
}

// 4
let apple = Manufacturer(name: "Apple")
// 5
let iPhone = Device(name: "iPhone", manufacturer: apple)
let iPad = Device(name: "iPad", manufacturer: apple)

// 6
iPad.manufacturer.name = "Google"

// 7
print(iPhone.manufacturer.name)
print(iPad.manufacturer.name)

In the code above, you will:

  1. Create a Manufacturer struct with a name property.
  2. Create a Device class with a name and manufacturer properties.
  3. Declare the manufacturer of the device.
  4. Create a manufacturer named "Apple".
  5. Create two devices, "iPhone" and "iPad".
  6. Apple sold the iPad division to "Google".
  7. Print the two manufacturer names.

You'll see that the two devices now have different manufacturers. The iPhone has Apple as the manufacturer and the iPad has Google as the manufacturer.

This is an example of how mixing value types in reference types works. Even if you are using the same instance of Manufacturer, two copies will be created when the instance is inside a reference type.

Mixing Reference Types in Value Types

Now we'll go over what happens when you mix reference types inside value types.

This time you'll have two airplanes and one engine. You'll define an Airplane struct and an Engine class. Of course, an airplane has an engine.

Add the following code:

// 1
class Engine: CustomStringConvertible {
    var description: String {
        return "\(type) Engine"
    }

    var type: String

    init(type: String) {
        self.type = type
    }
}

// 2
struct Airplane {
    var engine: Engine
}

// 3
let jetEngine = Engine(type: "Jet")
// 4
let bigAirplane = Airplane(engine: jetEngine)
let littleAirplane = Airplane(engine: jetEngine)

// 5
littleAirplane.engine.type = "Rocket"

// 6
print(bigAirplane)
print(littleAirplane)

In the code above, you will:

  1. Create the Engine class, with its type.
  2. Create the Airplane struct, with an engine.
  3. Create a jet engine.
  4. Create two airplanes, with the same Engine instance.
  5. Change the littleAirplane engine to a "Rocket" one.
  6. Print the two airplane objects.

As you can see, both of the airplanes have the same engine, the Rocket Engine. This is because you're referencing the same Engine instance. Even if you created two instances of an Airplane, with the same Engine, they won’t copy the reference type like value types.

X-Jet in X-Men First ClassSource: Marvel Entertainment

Pointers

In Swift, you can refer to an instance by using the inout keyword in function signatures. Using inout means that modifying the local variable will also modify the passed parameters. Without it, the passed parameters will remain the same value.

Add the following code:

// 1
func addKm(to bike: inout Bike, km: Float) {
    bike.km += km
}

// 2
var awesomeBike = Bike(radius: 22, km: 20)

// 3
addKm(to: &awesomeBike, km: 10)

// 4
print(awesomeBike)

In the code above, you will:

  1. Create an addKm function that takes an inout Bike parameter.
  2. Create the new Bike instance, named awesomeBike.
  3. Call the addKm with the & operator. The & operator tells the function to pass the address of the variable instead of a copy of it.
  4. Print the awesomeBike instance.

You'll see that by printing the awesomeBike variable, its km will be 30 because you're not creating a copy of it. Instead, you are using the same instance.

Reference types work in the same way. You can pass them with inout or not, and the result will be the same.

Add this code to the playground:

func addKm(to motorbike: inout Motorbike, km: Float) {
    motorbike.km += km
}

var awesomeMotorbike = Motorbike(radius: 30, km: 25)

addKm(to: &awesomeMotorbike, km: 15)

print(awesomeMotorbike.km)

Just like before, you've created a new Motorbike instance, named motorbike and passed it to the addKm function. The final print will print 40 km.

Now add the following code:

func addKm(to motorbike: Motorbike, km: Float) {
    motorbike.km += km
}

addKm(to: awesomeMotorbike, km: 15)

print(awesomeMotorbike.km)

The results are the same. Your final motorbike instance now has 55 km.

Summary

Now you've learned all the differences between value types and reference types in Swift, how to use them, how they work, and which one better fits your needs.

You can download the final playground for this post here.

One great advantage of using the default Swift value types like Array is the Copy on Write feature. That can be your next step in learning the differences between value types and reference types!

Here is a list of other useful links:

If we missed anything, let us know in the comments section below!

Visit Codementor Events

Last updated on May 30, 2022