Understanding reference types

In the previous chapters, you've been creating classes to contain your app's logic and user interface. This works perfectly fine, and to make use of these classes, we didn't need to know anything about how classes behave on the inside and how they manifest themselves in terms of memory and mutability. To understand value types and how they compare to reference types, it's essential that you do have an understanding of what's going on under the hood.

In Swift, there are two types of object that are considered a reference type: classes and closures. Classes are the only objects in Swift that can inherit from other classes. More on this later, when we discuss structs. Let's examine what it means for an object to be a reference type first.

Whenever you create an instance of a class, you have an object that you can use and pass around. For example, you could use an instance of a class as an argument of a method. This is demonstrated in the following snippet:

class Pet { 
var name: String

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

func printName(forPet pet: Pet) {
print(pet.name)
}

let cat = Pet(name: "Bubbles")
printName(forPet: cat) // Bubbles

The preceding code isn't too exciting. We pass an instance of the class Pet to the printName function and it prints our pet's name. By passing the instance of Pet to a function, we actually passed a reference from our Pet instance to the function. This might sound confusing, and before everything becomes clear, let's make things a little bit more confusing. If you change the printName function as displayed in the following snippet, you will see some behavior that probably confuses you just a bit more:

func printName(forPet pet: Pet) { 
print(pet.name)
pet.name = "Jeff"
}

let cat = Pet(name: "Bubbles")
printName(forPet: cat) // Bubbles
print(cat.name) // Jeff

After calling the printName function, our cat has a different name. You probably consider this strange; how does setting the name of the pet you received in the function change the name of the cat variable? If you've programmed in other languages, you might consider this obvious; after all, didn't we just pass that exact constant to the printName function? Also, doesn't it make sense, then, that the name of the original constant also changed? If this is what you thought, then your mindset is in the world of reference types. If this isn't what you thought, you expected our constant to behave more like a value type, which isn't a bad thing at all. However, you still don't really know what exactly is happening with our Pet instance.

The snippet we used to change the Pet's name is small, but it shows exactly how reference types work. Whenever you create an instance of a class, some space in memory is allocated to the instance. When you pass the instance as an argument to a function, you don't pass the contents of the instance. Instead, you pass the memory address or, in other words, a reference to the instance. Unless you explicitly copy a reference type, every time you pass your instance around or point another variable to it, the same address in memory is changed. Let's expand our initial example a bit more, so this becomes even more apparent:

let cat = Pet(name: "Bubbles") 
let dog = cat

printName(forPet: cat) // Bubbles
dog.name = "Benno"

print(cat.name) // Benno

Here, we created a cat constant, then we created a dog constant and set the cat constant as its value. Then, we called the printName function; this works as before. Next, we changed the name of the dog. When we print the name of the cat instance, it's not what you might expect at first because the name of the dog is printed. This is because we didn't copy anything, we simply pointed the dog constant to the same address in memory that the cat pointed to.

Reference types are passed around by the address in memory they point to. Unless you explicitly copy an object, multiple variables or constants point to the same instance of the object. As a result of this, mutating one of these variables will mutate them all.

Although this behavior can be convenient at certain times, it could also turn out to pack some nasty surprises if your apps grow to a substantial size. Therefore, it's a good idea to understand value types and know when to use them, because value types can save you quite some debugging time.