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.
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.
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:
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:
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:
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.
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:
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:
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:
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"
:
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.
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.