Skip to main content
Photo from unsplash

Understanding Swift's Value and Reference Types

Written on May 10, 2022 by Pramesh Ojha.

8 min read

Introduction

There are two kinds of types in Swift, which are Value and Reference Types. These types and their characteristics sometimes can be hard to remember and understand. Through this post, I'll try to explain it using a mental model and analogy which will help you easily master swift types.

Primitives

If you use common programming languages (Java, JavaScript, etc) before, you must be familiar with primitives and non-primitives data types. I'm not going to jump into the details of primitives, but here are some illustrations I got from google.

other-primitive-types

Usually, primitives conclude specific data types such as boolean, char, integer, and float.

Does Swift have primitive types?

No, Swift doesn't have primitive types. In a sense. Swift still provides 'primitive-like' data types such as Int, Bool, Double, etc. However, they are made with struct.

If you look into Swift's Int type definition, you can see that it is made with a struct

primitive-made-with-struct

Interesting right?


Quick Intro to Mental Model

A mental model is an explanation of someone's thought process about how something works in the real world. It is a representation of the surrounding world. - Wikipedia

You might be familiar with this variable box analogy:

box-analogy

We think of variables as containers that hold information and allow us to access them later. We will think of this as a box that has a label on it. - StudeApps

This works wonders when you are trying to understand what a variable does.

That is a mental model. You create a certain type of analogy to help you understand a concept.

The prior analogy is not a one-size-fits-all, I won't be using it to explain value & reference type. So prepare for some changes 💪


Value and Reference Types

types-in-swift

There are two kinds of types in Swift which are Value Types, and Reference Types. Value types are usually defined as struct, enum, and tuple. Whereas the latter is usually defined as a class

Wire Analogy

I'm going to use a new mental model for variables, which uses a wire to point to the value it holds.

wire-analogy

Therefore each variable can point to a single value according to its data type.


Value Types

value-types

A value type is a type whose value is copied when it's assigned to a variable or constant, or when it's passed to a function - Swift Docs

Remember that 'primitive' data types like Int, Double, String, etc. are made with struct. So they follow the value type mental model.

Mental Model

Let's say we have a struct of Animal (the behavior is also the same with enum, tuple, also Int, String because they're made with struct)

struct Animal {
    var legs = 4
}
 
var sheep = Animal()
types-model-1

Then, we are assigning the cow variables with the value of sheep

var cow = sheep

Key point: the value will be copied.

types-model-2

Effect of Copying

After we copy, the sheep and cow variables now points to two different struct. Therefore if we mutate the cow, the sheep won't get affected, and vice versa.

struct Animal {
    var legs = 4
}
 
var sheep = Animal()
var cow = sheep
 
// mutating cow's property
cow.legs = 3
 
print(sheep.legs) // 4
print(cow.legs) // 3
types-copy-effect

Reference Types

reference-types

A reference types is where instances share a single copy of the data when they're assigned to a variable or constant, or when they're passed to a function.

In the wire analogy, it will point to the same value. We're using a class that behaves as a reference type.

Mental Model

class Animal {
    var legs = 4
}
 
var sheep = Animal()
var cow = sheep

Key Point: It will share a single copy

reference-model

Effect of Sharing A Single Copy

I believe you already guessed correctly how it will behave. If we mutate one variable, both will be affected.

class Animal {
    var legs = 4
}
 
var sheep = Animal()
var cow = sheep
 
// mutating cow's property
cow.legs = 3
 
print(sheep.legs) // 3
print(cow.legs) // 3
reference-copy-effect

Proof

To prove that it is sharing a single copy, we can use === (identity equality). It will return true if two reference point to the same object instance.

Let's throw in a new instance called pig

var sheep = Animal()
var cow = sheep
 
// created a new instance
var pig = Animal()

Here's the wire

reference-proof

Then we can compare them using identity equality

print(sheep === cow) // true
print(sheep === pig) // false

When in doubt, draw the wire analogy to help you. I'm using excalidraw for the illustration

Reference Types Inside of Value Types

Important thing to note is: If you are referencing a class inside of a struct, then that variable will still behave like the reference type

class Leg {
    var count = 4
}
 
struct Animal {
    var name: String
    var legs = Leg()
}
 
var sheep = Animal(name: "Sheep")
var cow = sheep
 
sheep.legs.count = 3
 
print(sheep.legs.count) // 3
print(cow.legs.count) // 3
 
// referencing the same class
print("\(sheep.legs === sheep.legs)") // true
reference-inside-value

Additional Emphasis

I need to emphasize this in case you're coming from a JavaScript background.

In Swift, Array and Dictionary are all value types.

array-struct

It is still made with struct 😬

How to Choose?

I don't have much experience with this yet, so I'll quote an article instead

Use a value type when:

  • Comparing instance data with == makes sense
  • You want copies to have an independent state
  • The data will be used in code across multiple threads

Use a reference type (e.g. use a class) when:

  • Comparing instance identity with === makes sense
  • You want to create a shared, mutable state

I believe that using value type for overall use will be sufficient. We can trust that when we change one variable/property, it won't affect the others. Thus, creating a sense of safety and reliability.

Keep a note that this difference only happens when you mutate. In absence of mutation, values and references act exactly the same way.

Functions & In-Out

Function parameter follows value types. This means you can't mutate the parameter and change the value.

function-unable-to-mutate

Swift won't even let you mutate them. Because what is passed in the parameter will be converted into a let variable.

You can imitate reference types on function parameter by using inout

var numbers = [1,2,3]
 
func foo(_ arr: inout Array<Int>)  {
    arr.removeLast()
}
 
foo(&numbers)
print(numbers) // [1,2]

Notice the &(ampersand) which is an explicit recognition that you're aware it is being used as inout.

Under the hood, the In-Out parameter doesn't use reference types.

This behavior is known as copy-in copy-out or call by value result. For example, when a computed property or a property with observers is passed as an in-out parameter, its getter is called as part of the function call and its setter is called as part of the function return. - Swift Docs

Conclusion

You now understand that:

  • Swift 'primitive-like' variables are made with a struct
  • Value types will copy the value if assigned to a variable or passed into a function
  • Reference types will share a single instance if assigned to a variable or passed into a function
  • Mutating value types won't affect the other copy, on the other hand, mutating reference types will affect the single instance
  • Function parameters follows value types, but can imitate reference types by using the in-out parameter
Share this article

Enjoying this post?

Don't miss out 😉. Get an email whenever I post, no spam.

Subscribe Now