Swift tutorial: Reference types

The other day, I was chatting with some colleagues, who were interviewing people for a Swift Dev job opening. They said the killer question was, what’s the difference between a ‘struct’ and a ‘class’?. I was surprised. That’s something every swift developer should know. For that reason, I decided to write this post.

what is a reference?

Imagine a reference as a link to a value. With a reference, we access the data. Instances of a class are called “objects”, and you cannot directly assign them to a variable, you reference it.

The most common way to create a reference type in Swift is using a class

1
2
3
class MyClass {
var a = 1
}

Unlike the value types, references do change their referenced value when the assigned variable is mutated.

1
2
3
4
var myClassInstance = MyClass()
var anotherInstance = myClassInstance
anotherInstance.a = 2
myClassInstance.a

Even when a reference is assigned with let, we can still modify properties in the referenced object.

1
2
3
4
let constantReference = MyClass()
constantReference.a // = 1
constantReference.a = 3
constantReference.a // = 3

That means that the constant is the reference, not the object. With value types we wouldn’t be able to do this.

1
2
3
4
5
6
struct MyStruct {
var b = 5
}
let myValue = MyStruct()
myValue.b = 8 // Error! 🤯

Trying to change the value of b in myValue.b = 8 would result in a compiler error.

Now let’s complicate things a little. Let’s create a value type that contains a reference type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ReferenceType {
var aValueProperty = 5
}
struct ValueType {
var aReference = ReferenceType()
var aValue = "hello"
}
// Create the value
let valueInstance = ValueType()
// copy it
var anotherValueInstance = valueInstance
// change it's properties
anotherValueInstance.aReference.aValueProperty = 1000
anotherValueInstance.aValue = "bye"
// Now lets check what changed in the original
valueInstance.aReference.aValueProperty // 1000
valueInstance.aValue // "hello"

Notice how aValue maintained it’s original value in valueInstance, whereas aReference.aValueProperty changed for both the original and the copied value, even when it was only modified in the copy.

This behaviour is what we call a “shallow copy”, where we copy all the values but the referenced value stays the same. This is because we only copied the references. On the other hand, when we copy absolutely everything, including the referenced values, we call this a “deep copy”.

This may be a little confusing, so take your time to understand it.

Equality of Reference Types

When we use the equals operator (==), we are checking if two values are the same.

1
2
3
4
5
6
1 == 1 // true
let x = 400
let y = 250
x == y // false

The same behaviour doesn’t occur when comparing custom value types.

1
2
3
4
5
6
7
8
struct AnotherValueType {
let aValue = 10
}
let m = AnotherValueType()
let n = m
m == n // Error: Binary operator '==' cannot be applied to two 'AnotherValueType' operands

This behaviour happened because AnotherValueType doesn’t implement the Equatable protocol, but that’s a topic for another time.

The same happens for reference types. What we can do with reference types is check if they are the same with the === operator

1
2
3
4
5
6
7
8
class AnotherClass {
let x = [1,2,3]
}
let j = AnotherClass()
let k = j
j == k // Error!
j === k // 👍

The === operator checks if two references point to the same value.

Functions

Surprise! Yes, functions are reference types.

You can assign them to variables:

1
2
3
let aFunction = {
return 50
}

You can pass them as parameters:

1
2
3
4
5
func anotherFunction(aFunctionAsParameter: () -> Int) -> Int {
return aFunctionAsParameter()
}
anotherFunction(aFunctionAsParameter: aFunction)

They are one of the most relevant features of Swift.

These kinds of functions that receive another function as a parameter are called “higher order functions”. Some famous ones are: map, reduce, filter, among others.

1
2
3
4
5
6
7
8
let grades = [10,5,3,9]
// lets see who passed:
let passed = grades.filter { (aGrade) -> Bool in
return aGrade >= 5
}
// passed = [10,5,9]

Functions can be declared inside other functions, like in a for-loop or inside many other scopes. But since functions are reference types when they are declared inside another scope and passed further, after the local scope ends, the local variables used inside the function aren’t destroyed.

1
2
3
4
5
6
7
8
func outerFunction() -> () -> String {
let aVariable = "Hello from outer function"
let innerFunction = {
return aVariable
}
return innerFunction
}
outerFunction()()

The functions that hold variables outside of their scope are usually called closures. But it doesn’t mean that other functions declared with func aren’t also closures.

Not The End

Having a good understanding of how types work in Swift will help you decide when to use what, and more significantly, to better architect your apps.

Please, let me know your thoughts and questions.

Partager Commentaires