Get up to 80 % extra points for free! More info:

Lesson 4 - Reference and value data types in Swift

In the previous lesson, RollingDie in Swift - Constructors and Random numbers, we created our first regular object in Swift, a rolling die.

Objects are reference data types that behave in a different way than value data types, e.g. Int, in certain aspects. It's important to know what exactly is going on inside the program, otherwise, we'd end up with undesired results.

We'll go over value types once more before we move on. Generally, value types are simple structures, e.g. one number, one character. We work with them mostly to get the job done as fast as possible. There are usually a lot of them in a program and occupy a small amount of memory. They're often described as "light-weight" structures. They each have a fixed size. Examples of value types include Int, Float, Double, Character, Bool, and others.

An application, more so, its thread, allocates memory from the operating system in the form of a stack. It accesses this memory at very high speeds, but the application can't control its size and the resources are assigned by the operating system. This small, fast memory is used to store local variables of the value type, with some exceptions in the iterations which we'll get into later on. Here's a visual representation of the memory:

Stack in computer memory - Object-Oriented Programming in Swift

The image above shows the memory available to be used by our application. We've created a variable a of the Int data type in the application. Its value is 56 and was stored directly into the stack. The corresponding code might look something like this:

let a : `Int` = 56;

You could think of it as the a variable having an allocated part of memory in the stack, of the size of the Int data type which is 32 bits, where the value 56 is stored.

Let's create a new console application and add a simple class that will represent a "user". For clarity, we'll omit comments and won't bother with access modifiers:

class User {
    var age : Int
    var name : String

    init(name: String, age: `Int`) {
        self.name = name
        self.age = age
    }
}

The class has two simple public fields and a constructor. Let's create an instance of this class in our main program:

let a : Int = 56
let u = User(name: "James Brown", age: 28)

A variable u is now of the reference data type. Let's see how this situation looks like in memory:

Stack and heap in computer memory - Object-Oriented Programming in Swift

We can see that an object, a variable of the reference data type, is not stored in the stack, but in the memory called the heap. It's for this very reason that objects are generally more complicated than value data types. They usually contain other fields and occupy more space in memory.

Both stack and heap are located in the RAM memory. The difference is in the access and in the size. The heap is almost unlimited memory, which is, however, complicated to access so it ends up being slower. On the other hand, the stack memory is fast, but limited in size.

Reference-type variables are actually stored in the memory twice, once in the stack and once in the heap. Within the stack there is something we call a reference, a link to the heap where an actual object can be found.

For example, in C++, there's a huge difference between pointers and references. Swift uses the "reference" term, whose principles are paradoxically closer to those of C++ pointers. The "reference" term mentioned here stands for the Swift reference and has nothing to do with C++.

There are several reasons why things are done this way:

  1. The stack size is limited.
  2. When we want to use the same object multiple times, e.g. to pass it as a parameter into several methods, we don't need to copy it. We only have to pass a small value type containing the reference to the object instead of copying a whole heavy-weight object.
  3. Thanks to references we are able to create structures with dynamic size easily, for example array-like structures in which we can add new elements at run-time. These elements reference each other, like a string of objects.

Now let's declare two variables of the Int type and two variables of the User type:

var a : Int = 56
var b : Int = 28
var u = User(name: "James Brown", age: 28)
var v = User(name: "Jack White", age: 32)

Here's what this would look like in memory:

Reference values in computer memory in Swift - Object-Oriented Programming in Swift

Now let's assign the b variable to the a variable. We'll also assign the v variable to the u variable. During value assignments, value types are just copied to the stack. Alternatively, when it comes to objects, only its reference is copied (which is in fact a value type too). Assigning references does not create new objects. Now, our code should look something like this:

var a : Int = 56
var b : Int = 28
var u = User(name: "James Brown", age: 28)
var v = User(name: "Jack White", age: 32)
a = b
u = v

Memory-wise, it would look like so:

Reference values in computer memory in Swift - Object-Oriented Programming in Swift

Now, let's verify the reference mechanism, so we can confirm that it truly works this way :) First, we'll print all 4 variables before and after re-assigment. Since, we'll be printing several times, I will make the snippet short. Let's modify the code:

// variable declaration
var a : Int = 56
var b : Int = 28
var u = User(name: "James Brown", age: 28)
var v = User(name: "Jack White", age: 32)
print("a: \(a)\nb: \(b)\nu: \(u.name)\nv: \(v.name)\n")

// assignment
a = b
u = v
print("a: \(a)\nb: \(b)\nu: \(u.name)\nv: \(v.name)\n")

We still can't tell what the difference is between value and reference data types are based on the output:

a: 56
b: 28
u: James Brown
v: Jack White

a: 28
b: 28
u: Jack White
v: Jack White

However, we do know that while a and b are really two different numbers with the same value, u and v is the exact same object. Let's change the name of the user v and based off what we know, the change should be reflected in the variable u:

// change
v.name = "John Doe"
print("u: \(u.name)\nv: \(v.name)\n")

We've changed the object in the variable v. Now let's print u and v once more:

Console application
a: 56
b: 28
u: James Brown
v: Jack White

a: 28
b: 28
u: Jack White
v: Jack White

u: John Doe
v: John Doe

The user u changes along with v because both variables point to the same object. If you're asking how to create a true copy of an object, the easiest way is to re-create the object by using the constructor and initializing the new object with the same data. We can also clone objects, but we'll go over that some other time. Let's get back to James Brown:

Reference types in computer memory in Swift - Object-Oriented Programming in Swift

Now what will happen to him, you ask? He'll be "eaten" by what we call the ARC (Automatic Reference Counting). The picture below is only illustrative; Garbage Collector is an alternative term to ARC in other languages.

ARC in Swift - Object-Oriented Programming in Swift

ARC and dynamic memory management

We can allocate memory statically in our programs, meaning that we declare how much memory we'll need in the source code. We've done it several times already and had no problems doing it. We have written the necessary variables in the source code, but soon, we'll make applications, where we won't know how much memory we'll need before we run it.

In the past, particularly in the era of the languages C, Pascal, and C++, direct memory pointers were used. Altogether, it worked like this: we'd ask the operating system for a piece of memory of certain size. Then, it would reserve it for us and give us its address. We would then create a pointer to this place, through which we worked with the memory. The problem was that no one was looking after what we put into this memory, the pointer just pointed to the beginning of the reserved memory. When we put something larger there, it would be simply stored anyway and overwrite the data beyond our memory's limits, which belonged to some another program or even to the operating system (in this case, OS would probably kill or stop our application). We would often overwrite our program's data in the memory and the program would start to behave chaotically. Imagine that you add a user to an array and it ends up changing the user's environment color which is something that has nothing to do with it. You would spend hours checking the code for mistakes, and you would end up finding out that there's a memory leak in the user's creation that overflew into the color values in memory.

The other problem was when we stopped using an object, we had to free its memory manually, and if we didn't, the memory would remain occupied. If we did this in a method and forgot to free the memory, our application would start to freeze. Eventually, it would crash the entire operating system. An error like this is very hard to pin-point. Why does the program stop working after a few hours? Where in thousands of lines of code should we look for the mistake? We have no clue. We can't follow anything, so we'd end up having to look through the entire program line by line or examining the computer memory which is in binary. cringes. A similar problem occurs when we free memory somewhere and then use the same pointer again, forgetting it has been already freed, it would point to a place where something new might be already stored, and we would corrupt this data. It would lead to uncontrollable behavior in our application and it could even lead to this:

Blue Screen Of Death – BSOD in Windows - Object-Oriented Programming in Swift

ARC is a part of the application code and it'll be inserted automatically to our application by the compiler when building our application. We already know that the abbreviation refers to Automatic Reference Counting. Once the number of references to some object reaches zero, ARC will deallocate it and remove the object from memory. The program reaches the stage where it knows that it can't get to the object anymore and therefore it occupies unnecessary space. Later, we'll get to the weak keyword with which we can mark some variables. Weak references are not included in the "ARC Counter", and as soon as there are only weak references, deallocation occurs.

The null value

The last thing I'll mention here (or remind) is the nil value we already know from the basic course, the lesson about the Optional type. Unlike other languages with the null equivalent, in Swift, we can't just assign the nil value to a variable, the variable must be declared as Optional first.

When we set a variable to nil, we only destroy that one reference. If there is still some reference to the object, it will still exist. If not, ARC will do its job.

We'll get back to reference types in future.

In the next lesson, Warrior for the arena in Swift, we'll program something practical again to gain experience. Spoiler: we're making a warrior object for the arena :)


 

Previous article
RollingDie in Swift - Constructors and Random numbers
All articles in this section
Object-Oriented Programming in Swift
Skip article
(not recommended)
Warrior for the arena in Swift
Article has been written for you by Filip Němeček
Avatar
User rating:
2 votes
Activities