Value types versus reference types

All variables and constants in Swift are stored in memory. In fact, unless you explicitly write data to the file system, everything you create is going to be in memory. In Swift, there are two different categories of types. These two categories are value types and reference types. The only way in which they differ is in the way they behave when they get assigned to new variables, passed into methods, or captured in closures. Essentially, they only differ when you try to assign a new variable or constant to the value of an existing variable or constant.

A value type is always copied when being assigned somewhere new while a reference type is not. Before we look at exactly what that means in more detail, let's go over how we determine if a type is a value type or a reference type.

Determining value type or reference type

A value type is any type that is defined as either a structure or an enumeration, while all classes are reference types. This is easy to determine for your own custom types based on how you declared them. Beyond that, all of the built-in types for Swift, such as strings, arrays, and dictionaries are value types. If you are ever uncertain, you can test any of the two types you want in a playground, to see if its behavior is consistent with a value type or a reference type. The simplest behavior to check is what happens on assignment.

Behavior on assignment

When a value type is reassigned, it is copied so that afterwards each variable or constant holds a distinct value that can be changed independently. Let's take a look at a simple example using a string:

var value1 = "Hello"
var value2 = value1
value1 += " World!"
print(value1) // "Hello World!"
print(value2) // "Hello"

As you can see, when value2 is set to value1 a copy gets created. This is so that when we append " World!" to value1, value2 remains unchanged, as "Hello". We can visualize them as two completely separate entities:

Behavior on assignment

On the other hand, let's take a look at what happens with a reference type:

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}
var reference1 = Person(name: "Kai")
var reference2 = reference1
reference1.name = "Naya"
print(reference1.name) // "Naya"
print(reference2.name) // "Naya"

As you can see, when we changed the name of reference1, reference2 was also changed. So why is this? As the name implies, reference types are simply references to an instance. When you assign a reference to another variable or constant, both are actually referring to the exact same instance. We can visualize it as two separate objects referencing the same instance:

Behavior on assignment

In the real world, this would be like two kids sharing a toy. Both can play with the toy but if one breaks the toy, it is broken for both kids.

However, it is important to realize that if you assign a reference type to a new value, it does not change the value it was originally referencing:

reference2 = Person(name: "Kai")
print(reference1.name) // "Naya"
print(reference2.name) // "Kai"

As you can see, we assigned reference2 to an entirely different Person instance, so they can now be manipulated independently. We can then visualize this as two separate references on two separate instances, as shown in the following image:

Behavior on assignment

This will be like buying a new toy for one of the kids.

This shows you that a reference type is actually a special version of a value type. The difference is that a reference type is not itself an instance of any type. It is simply a way to refer to another instance, sort of like a placeholder. You can copy the reference so that you have two variables referencing the same instance, or you can give a variable a completely new reference to a new instance. With reference types, there is an extra layer of indirection based on sharing instances between multiple variables.

Now that we know this, the simplest way to verify if a type is a value type or a reference type is to check its behavior when being assigned. If the second value is changed when you modify the first value, it means that the type you are testing is a reference type.

Behavior on input

Another place where the behavior of a value type differs from a reference type is when passing them into functions and methods. However, the behavior is very simple to remember if you look at passing a variable or constant into a function as just another assignment. This means that when you pass a value type into a function, it is copied while a reference type still shares the same instance:

func setNameOfPerson(person: Person, var to name: String) {
    person.name = name
    name = "Other Name"
}

Here we have defined a function that takes both a reference type: Person and a value type: String. When we update the Person type within the function, the person we passed in is also changed:

var person = Person(name: "Sarah")
var newName = "Jamison"
setNameOfPerson(person, to: newName)

print(person.name) // "Jamison"
print(newName) // "Jamison"

However, when we change the string within the function, the String passed into it remains unchanged.

The place where things get a little more complicated is with inout parameters. An inout parameter is actually a reference to the passed-in instance. This means that, it will treat a value type as if it were a reference type:

func updateString(inout string: String) {
    string = "Other String"
}

var someString = "Some String"
updateString(&someString)
print(someString) // "Other String"

As you can see, when we changed the inout version of string within the function, it also changed the someString variable outside of the function just as if it were a reference type.

If we remember that a reference type is just a special version of a value type where the value is a reference, we can infer what will be possible with an inout version of a reference type. When we define an inout reference type, we actually have a reference to a reference; this reference is then the one that is pointing to a reference. We can visualize the difference between an inout value type and an inout reference type as shown:

Behavior on input

If we simply change the value of this variable, we will get the same behavior as if it were not an inout parameter. However, we can also change where the inner reference is referring to by declaring it as an inout parameter:

func updatePerson(inout insidePerson: Person) {
    insidePerson.name = "New Name"
    insidePerson = Person(name: "New Person")
}

var person2 = person
updatePerson(&person)
print(person.name) // "New Person"
print(person2.name) // "New Name"

We start by creating a second reference: person2 to the same instance as the person variable that currently has the name "Jamison" from before. After this, we pass the original person variable into our updatePerson: method and have this:

Behavior on input

In this method, we first change the name of the existing person to a new name. We can see in the output that the name of person2 has also changed, because both insidePerson inside the function and person2 are still referencing the same instance:

Behavior on input

However, we then also assign insidePerson to a completely new instance of the Person reference type. This results in person and person2 outside of the function pointing at two completely different instances of Person leaving the name of person2 to be "New Name" and updating the name of person to "New Person":

Behavior on input

Here, by defining insidePerson as an inout parameter, we were able to change where the passed-in variable was referencing. It can help us to visualize all the different types as one type pointing to another.

At any point, any of these arrows can be pointed at something new using an assignment and the instance can always be accessed through the references.

Closure capture behavior

The last behavior we have to worry about is when variables are captured within closures. This is what we did not cover about closures in Chapter 5, A Modern Paradigm – Closures and Functional Programming. Closures can actually use the variables that were defined in the same scope as the closure itself:

var nameToPrint = "Kai"
var printName = {
    print(nameToPrint)
}
printName() // "Kai"

This is very different from normal parameters that we have seen before. We actually do not specify nameToPrint as a parameter, nor do we pass it in when calling the method. Instead, the closure captures the nameToPrint variable that is defined before it. These types of captures act similarly to inout parameters in functions.

When a value type is captured, it can be changed and it will change the original value as well:

var outsideName = "Kai"
var setName = {
    outsideName = "New Name"
}
print(outsideName) // "Kai"
setName()
print(outsideName) // "New Name"

As you can see, outsideName was changed after the closure was called. This is exactly like an inout parameter.

When a reference type is captured, any changes will also be applied to the outside version of the variable:

var outsidePerson = Person(name: "Kai")
var setPersonName = {
    outsidePerson.name = "New Name"
}
print(outsidePerson.name) // "Kai"
setPersonName()
print(outsidePerson.name) // "New Name"

This is also exactly like an inout parameter.

The other part of closure capture that we need to keep in mind is that changing the captured value after the closure is defined will still affect the value within the closure. We can take advantage of this to use the printName closure we defined in the preceding section to print any name:

nameToPrint = "Kai"
printName() // Kai
nameToPrint = "New Name"
printName() // "New Name"

As you can see, we can change what printName prints out by changing the value of nameToPrint. This behavior is actually very hard to track down when it happens accidently, so it is usually a good idea to avoid capturing variables in closures whenever possible. In this case, we are taking advantage of the behavior, but more often than not, it will cause bugs. Here, it would be better to pass what we want to print as an argument.

Another way to avoid this behavior is to use a feature called capture lists. With this, you can specify the variables that you want to capture by copying them:

nameToPrint = "Original Name"
var printNameWithCapture = { [nameToPrint] in
    print(nameToPrint)
}
printNameWithCapture() // "Original Name"
nameToPrint = "New Name"
printNameWithCapture() // "Original Name"

A capture list is defined at the beginning of a closure before any parameter. It is a comma-separated list of all the variables being captured, which we want to copy within square brackets. In this case, we requested nameToPrint to be copied, so when we change it later, it does not affect the value that is printed out. We will see more advanced uses of capture lists later in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset