Chapter 7. Everything Is Connected – Memory Management
When using an app, there is nothing worse than it being slow and unresponsive. Computer users have come to expect every piece of software to respond immediately to every interaction. Even the most feature-rich app will be ruined if it is unpleasant to use because it doesn't manage the device resources effectively. Also, with the growing popularity of mobile computers and devices, it is more important than ever to write software that uses battery power efficiently. One of the aspects of writing software that has the largest impact on both responsiveness and battery power is memory management.
In this chapter, we will discuss techniques specific to Swift that allow us to manage memory in order to ensure that our code remains responsive and minimizes its effect on battery life and other apps. We will do so by covering the following topics:
- Computer data storage
- Value types versus reference types
- Automatic reference counting
- Strong reference cycles
- Lost objects
- Structures versus classes
Computer data storage
Before we start looking at the code, we need to understand in some detail how data is represented in a computer. The common cliché is that all data in a computer is in 1s and 0s. This is true, but not so important when talking about memory management. Instead, we are concerned about where the data is stored. All computers, whether a desktop, laptop, tablet, or phone, store data in two places.
The first place we normally think of is the file system. It is stored on a dedicated piece of hardware; this is called a hard disk drive in many computers, but more recently, some computers have started to use solid-state drives. The other thing we hear about when buying computers is the amount of "memory" it has. Computer memory comes in "sticks" which hold less information than normal drives. All data, even if primarily stored on the Internet somewhere, must be loaded into the computer's memory so that we can interact with it.
Let's take a look at what that means for us as programmers.
File system
The file system is designed for long-term storage of data. It is far slower to access than memory, but it is much more cost effective for storing a lot of data. As the name implies, the file system is simply a hierarchical tree of files, which we as users can interact with directly using the Finder on a Mac. This file system still exists on iPhones and iPads but it is hidden from us. However, software can still read and write the file system, thus allowing us to store data permanently, even after turning the device off.
Memory
Memory is a little more complex than the file system. It is designed to store the necessary data, temporarily for the software running currently. Unlike with a file system, all memory is lost as soon as you turn off your device. The analogy is similar to how we humans have short-term and long-term memory. While we are having a conversation or thinking about something, we have a certain subset of the information we are actively thinking about and the rest is in our long-term memory. In order to actively think about something, we have to recall it from our long-term memory into our short-term memory.
Memory is quick to access, but it is much more expensive. When computers start to act abnormally slow, it is commonly because it is very close to using up all of its memory. This is because the operating system will automatically start using the file system as a backup when memory is low. Information that is meant for short-term storage is automatically written to the file system instead, making it much slower to access again.
This is similar to how we humans have a problem processing too much information at once. If we try to add two 20-digit numbers in our head, it is going to take us a long time or simply be impossible. Instead, we often write out the partial solution on paper, as we go along. In this case, the paper is acting as our file system. It would be faster if we could just remember everything instead of taking the time to write it down and read it back, but we simply can't process that much information at one time.
This is important to consider when programming because we want to reduce the amount of memory that we use at any given time. Using a lot of memory doesn't only negatively affect our own software; it can negatively affect the entire computer's performance. Also, when the operating system has to resort to using the file system, the extra processing and extra access to a second piece of hardware causes more power usage.
Now that we understand our goal, we can start discussing how we manage memory better in Swift.
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:
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.
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:
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.
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.
Automatic reference counting
Now that we understand the different ways in which data is represented in Swift, we can look into how we can manage the memory better. Every instance that we create takes up memory. Naturally, it wouldn't make sense to keep all data around forever. Swift needs to be able to free up memory so that it can be used for other purposes, once our program doesn't need it anymore. This is the key to managing memory in our apps. We need to make sure that Swift can free up all the memory that we no longer need, as soon as possible.
The way that Swift knows it can free up memory is when the code can no longer access an instance. If there is no longer any variable or constant referencing an instance, it can be repurposed for another instance. This is called "freeing the memory" or "deleting the object".
In Chapter 3, One Piece at a Time – Types, Scopes, and Projects we already discussed when a variable is accessible or not in the section about scopes. This makes memory management very simple for value types. Since value types are always copied when they are reassigned or passed into functions, they can be immediately deleted once they go out of scope. We can look at a simple example to get the full picture:
func printSomething() { let something = "Hello World!" print(something) }
Here we have a very simple function that prints out "Hello World!". When printSomething
is called, something
is assigned to a new instance of String
with the value "Hello World!"
. After print
is called, the function exits and therefore something
is no longer in scope. At that point, the memory being taken up by something
can be freed.
While this is very simple, reference types are much more complex. At a high level, an instance of a reference type is deleted at the point that there is no longer any reference to the instance in scope anymore. This is relatively straightforward to understand but it gets more complex in the details. The Swift feature that manages this is called Automatic Reference Counting or ARC for short.
Object relationships
The key to ARC is that every object has relationships with one or more variables. This can be extended to include the idea that all objects have a relationship with other objects. For example, a car object would contain objects for its four tires, engine, and so on. It will also have a relationship with its manufacturer, dealership, and owner. ARC uses these relationships to determine when an object can be deleted. In Swift, there are three different types of relationships: strong, weak, and unowned.
Strong
The first, and default type of relationship is a strong relationship. It says that a variable requires the instance it is referring to always exist, as long as the variable is still in scope. This is the only behavior available for value types. When an instance no longer has any strong relationships to it, it will be deleted.
A great example of this type of relationship is with a car that must have a steering wheel:
class SteeringWheel {} class Car { var steeringWheel: SteeringWheel init(steeringWheel: SteeringWheel) { self.steeringWheel = steeringWheel } }
By default, the steeringWheel
property has a strong relationship to the SteeringWheel
instance it is initialized with. Conceptually, this means that the car itself has a strong relationship to the steering wheel. As long as a car exists, it must have a relationship to a steering wheel that exists. Since steeringWheel
is declared as a variable, we could change the steering wheel of the car, which would remove the old strong relationship and add a new one, but a strong relationship will always exist.
If we were to create a new instance of Car
and store it in a variable, that variable would have a strong relationship to the car:
let wheel = SteeringWheel() let car = Car(steeringWheel: wheel)
Lets break down all the relationships in this code. First we create the wheel
constant and assign it to a new instance of SteeringWheel
. This sets up a strong relationship from wheel
to the new instance. We do the same thing with the car
constant, but this time we also pass in the wheel
constant to the initializer. Now, not only does car
have a strong relationship to the new Car
instance, but the Car
initializer also creates a strong relationship from the steeringWheel
property to the same instance as the wheel
constant:
So what does this relationship graph mean for memory management? At this time, the Car
instance has one strong relationship: the car
constant, and the SteeringWheel
instance has two strong relationships: the wheel
constant and the steeringWheel
property of the Car
instance.
This means that the Car
instance will be deleted as soon as the car
constant goes out of scope. On the other hand, the SteeringWheel
instance will only be deleted after both the wheel
constant goes out of scope and the Car
instance is deleted.
You can envision a strong reference counter on every instance in your program. Every time a strong relationship is setup to an instance the counter goes up. Every time an object strongly referencing it gets deleted, the counter goes down. If that counter ever goes back to zero, the instance is deleted.
The other important thing to realize is that all relationships are only in one direction. Just because the Car
instance has a strong relationship to the SteeringWheel
instance does not mean that the SteeringWheel
instance has any relationship back. You could add your own relationship back by adding a car property to the SteeringWheel
class, but you have to be careful when doing this, as we will see in the strong reference cycle section coming up.
Weak
The next type of relationship in Swift is a weak relationship. It allows one object to reference another without enforcing that it always exists. A weak relationship does not contribute to the reference counter of an instance, which means that the addition of a weak relationship does not increase the counter nor does it decrease the counter when removed.
Since a weak relationship cannot guarantee that it will always exist, it must always be defined as an optional. A weak relationship is defined using the weak
keyword before the variable declaration:
class SteeringWheel { weak var car: Car? }
This allows a SteeringWheel
to have a car assigned to it, without enforcing that the car never be deleted. The car initializer can then assign this backwards reference to itself:
class Car { var steeringWheel: SteeringWheel init(steeringWheel: SteeringWheel) { self.steeringWheel = steeringWheel self.steeringWheel.car = self } }
If the car is ever deleted, the car property of SteeringWheel
will automatically be set to nil. This allows us to gracefully handle the scenario that a weak relationship refers to an instance that has been deleted.
Unowned
The final type of relationship is an unowned relationship. This relationship is almost identical to a weak relationship. It also allows one object to reference another without contributing to the strong reference count. The only difference is that an unowned relationship does not need to be declared as optional and it uses the unowned
keyword instead of weak
. It acts similar to an implicitly unwrapped optional. You can interact with an unowned relationship as if it were a strong relationship, but if the unowned instance has been deleted and you try to access it, your entire program will crash. This means that you should only use unowned relationships in scenarios where the unowned object will never actually be deleted while the primary object still exists.
You may ask then, "Why would we not always use a strong relationship instead?" The answer is that sometimes unowned or weak references are needed to break something called a strong reference cycle.
Strong reference cycles
A strong reference cycle is when two instances directly or indirectly hold strong references to each other. This means that neither object can ever be deleted, because both are ensuring that the other will always exist.
This scenario is our first really bad memory management scenario. It is one thing to keep memory around longer than it is needed; it is a whole different level to create memory that can never be freed up to be reused again. This type of memory problem is called a memory leak, because the computer will slowly leak memory until there is no longer any new memory available. This is why you will sometimes see a speed improvement after restarting your device. Upon restart, all of the memory is freed up again. Modern operating systems will sometimes find ways to forcefully free up memory, especially when completely quitting an app, but we cannot rely on this as programmers.
So how can we prevent these strong reference cycles? First, let's take a look at what they look like. There are two main scenarios where these cycles can exist: between objects and with closures.
Between objects
A strong reference cycle between objects is when two types directly or indirectly contain strong references to each other.
Spotting
A great example of a strong reference cycle between objects is if we rewrite our preceding car example without using a weak reference from SteeringWheel
to Car
:
class SteeringWheel { var car: Car? } class Car { var steeringWheel: SteeringWheel init(steeringWheel: SteeringWheel) { self.steeringWheel = steeringWheel self.steeringWheel.car = self } }
The only difference between this code and the preceding code is that the car
property on SteeringWheel
is no longer declared as weak. This means that when a car is created, it will set up a strong relationship to the SteeringWheel
instance and then create a strong reference from the SteeringWheel
instance back to the car:
This scenario means that the reference count of both instances can never go down to zero and therefore they will never be deleted and the memory will be leaked.
Two objects can also indirectly hold strong references to each other through one or more third parties:
class Manufacturer { var cars: [Car] = [] } class SteeringWheel { var manufacturer: Manufacturer? } class Car { var steeringWheel: SteeringWheel? }
Here, we have the scenario where a Car
can have a strong reference to a SteeringWheel
that can have a strong reference to a Manufacturer
that in turn has a strong reference to the original Car
:
This is another strong reference cycle and it illustrates two more important points. First, optionals, by default, still create strong relationships when not nil. Also, the built in container types, such as arrays and dictionaries, also create strong relationships.
Clearly strong reference cycles can be difficult to spot, especially because they are hard to detect in the first place. An individual memory leak is rarely going to be noticeable to a user of your program, but if you continuously leak memory over and over again, it can cause their device to feel sluggish or even crash.
The best way as a developer to detect them is to use a tool built into Xcode called Instruments. Instruments can do many things, but one of those things is called Leaks. To run this tool you must have an Xcode Project; you cannot run it on a Playground. It is run by selecting Product | Profile from the menu bar.
This will build your project and display a series of profiling tools:
If you select the Leaks tool and press the record button in the upper-left corner, it will run your program and warn you of memory leaks which it can detect. A memory leak will look like a red X icon and will be listed as a leaked object:
You can even select the Cycles & Roots view for the leaked objects and Instruments will show you a visual representation of your strong reference cycle. In the following screenshot, you can see that there is a cycle between SteeringWheel
and Car
:
Clearly, Leaks is a powerful tool and you should run it periodically on your code, but it will not catch all strong reference cycles. The last line of defense is going to be you staying vigilant with your code, always thinking about the ownership graph.
Of course, spotting cycles is only part of the battle. The other part of the battle is fixing them.
Fixing
The easiest way to break a strong reference cycle is to simply remove one of the relationships completely. However, this is very often not going to be an option. A lot of the time, it is important to have a two-way relationship.
The way we fix cycles without completely removing a relationship is to make one or more of the relationships weak or unowned. In fact, this is the main reason that these other two types of relationships exist.
We fix the strong reference cycle in our original example by changing the car relationship back to weak:
class SteeringWheel { weak var car: Car? } class Car { var steeringWheel: SteeringWheel init(steeringWheel: SteeringWheel) { self.steeringWheel = steeringWheel self.steeringWheel.car = self } }
Now Car
has a strong reference to SteeringWheel
but there is only a weak reference back:
How you break any given cycle is going to depend on your implementation. The only important part is that somewhere in the cycle of references there is a weak or unowned relationship.
Unowned relationships are good for scenarios where the connection will never be missing. In our example, there are times that a SteeringWheel
exists without a car reference. If we change it so that the SteeringWheel
is created in the Car
initializer, we could make the reference unowned:
class SteeringWheel2 { unowned var car: Car init(car: Car) { self.car = car } } class Car { var steeringWheel: SteeringWheel2! init() { self.steeringWheel = SteeringWheel2(car: self) } }
Also, note that we had to define the steeringWheel
property as an implicitly unwrapped optional. This is because we had to use self
when initializing it but at the same time we cannot use self
until all the properties have a value. Making it optional allows it to be nil while we are using self
to create the steering wheel. This is safe as long as the SteeringWheel2
initializer doesn't try to access the steeringWheel
property of the passed in car.
With closures
As we found out in are just another type of object, so they follow the same ARC rules. However, they are subtler than classes because of their ability to capture variables from their surrounding scope. These captures create strong references from the closures to the captured variable that are often overlooked because capturing variables looks so natural compared to conditionals, for loops and other similar syntax.
Just as classes can create circular references, so can closures. Something can have a strong reference to a closure that directly or indirectly has a strong reference back to the original object. Let's take a look at how we can spot that.
Spotting
It is very common to provide closure properties that will be called whenever something occurs. These are generally called callbacks. Let's look at a ball class that has a callback for when the ball bounces:
class Ball { var location: (x: Double, y: Double) = (0,0) var onBounce: (() -> ())? }
This type of setup makes it easy to inadvertently create a strong reference cycle:
let ball = Ball() ball.onBounce = { print("\(ball.location.x), \(ball.location.y)") }
Here, we are printing out the location of the ball every time it bounces. However, if you consider this carefully, you will see that there is a strong reference cycle between the closure and the ball instance. This is because we are capturing the ball within the closure. As we have learned already, this creates a strong reference from the closure to the ball. The ball also has a strong reference to the closure through the onBounce
property. That is our circle.
You should always be conscious of what variables are being captured in your closures and if that variable directly or indirectly has a strong reference to the closure itself.
Fixing
To fix these types of strong reference cycles with closures we will again need to make one part of the circle weak or unowned.
Swift does not allow us to make closure references weak, so we have to find a way to capture the ball variable weakly instead of strongly.
To capture a variable weakly, we must use a capture list. Using a capture list, we can capture a weak or unowned copy of the original variable. We do so by specifying the weak
or unowned
variables before the capture list variable name:
ball.onBounce = { [weak ball] in print("\(ball?.location.x), \(ball?.location.y)") }
By declaring the ball copy as weak, it automatically makes it optional. This means that we had to use optional chaining to print out its location. Just like with other weak variables, ball
will be set to nil if the ball is deleted. However, based on the nature of the code, we know that this closure will never be called if ball
is deleted, since the closure is stored right on the ball instance. In that case, it is probably better to use the unowned
keyword:
ball.onBounce = { [unowned ball] in print("\(ball.location.x), \(ball.location.y)") }
It is always nice to clean up your code by removing unnecessary optionals.
Lost objects
It is a great idea to always keep strong reference cycles in mind, but if we are too aggressive with the use of weak and unowned references, we can run into the opposite problem, where an object is deleted before we intended it to be.
Between objects
With an object this will happen if all of the references to the object are weak or unowned. This won't be a fatal mistake if we use weak references, but if this happens with an unowned reference it will crash your program.
For example, let's look at the preceding example with an extra weak reference:
class SteeringWheel { weak var car: Car? } class Car { weak var steeringWheel: SteeringWheel! init(steeringWheel: SteeringWheel) { self.steeringWheel = steeringWheel steeringWheel.car = self } } let wheel = SteeringWheel() let car = Car(steeringWheel: wheel)
This code is the same as the preceding one except that both the car
property of SteeringWheel
and the steeringWheel
property of Car
are weak. This means that as soon as wheel
goes out of scope, it will be deleted, resetting the steeringWheel
property of the car to nil. There may be scenarios where you want this behavior, but often this will be unintentional and create confusing bugs.
The important thing is that you keep in mind all of the relationships an object has. There should always be at least one strong reference as long as you still want the object around and of course, there should never be a strong reference cycle.
With closures
This actually can't happen with closures because, as we discussed before, you cannot refer to a closure weakly. If you try, the compiler will give you an error:
class Ball2 { weak var onBounce: (() -> ())? // Error: 'weak' cannot be // applied to non-class type '() -> ()' }
Swift saves us from yet another type of bug.
Structures versus classes
Now that we have a good understanding of memory management, we are ready to discuss the full trade-offs we make when we choose to design a type as a structure or a class. With our ability to extend protocols like we saw in the previous chapter, we can achieve very similar functionality to the inheritance we saw with classes in Chapter 3, One Piece at a Time – Types, Scopes, and Projects. This means that we are often choosing between using a structure or a class based on the memory implications, or in other words, whether we want our type to be a value type or a reference type.
Value types have an advantage because they are very simple to reason about. You don't have to worry about multiple variables referencing the same instance. Even better, you don't have to worry about all of the potential problems we have discussed with strong reference cycles. However, there is still an advantage to reference types.
Reference types are advantageous when it really makes sense to share an instance between multiple variables. This is especially true when you are representing some sort of physical resource that makes no sense to copy like a port on the computer or the main window of an application. Also, some will argue that reference types use memory more efficiently, because it doesn't take up more memory with lots of copies floating around. However, the Swift compiler will actually do a lot of optimizing of our code and reduce or eliminate most of the copying that actually occurs when possible. For example, if we pass a value type into a function that never modifies the value, there is no reason to actually create that copy. Ultimately, I don't recommend optimizing for something like that before it becomes necessary. Sometimes you will run into memory problems with your application and then it can be appropriate to convert large types to classes if they are being copied a lot.
Ultimately, I recommend using structures and protocols as a default, because they greatly reduce complexity and fall back to classes only when it is required. I even recommend using protocols instead of super classes when possible, because they are easier to shift around and make it an easier transition between value types and reference types.
Summary
Memory management is often considered difficult to understand, but when you break it down, you can see that it is relatively straightforward. In this chapter, we have seen that all data in a computer is either stored in the file system that is a slow permanent storage, or in memory, which is a fast but temporary location. The file system is used as a backup to memory, slowing down the computer greatly, so we as programmers want to minimize the amount of memory we are ever using at one time.
We saw that in Swift there are value types and reference types. These concepts are critical to understanding how you can reduce memory usage and eliminate memory leaks. Memory leaks are created when an object has a strong reference to itself, maybe through a third party, which is called a strong reference cycle. We must also be careful that we keep at least one strong reference to every object we want to stay around or we may lose it prematurely.
With practice programming, you will get better with both preventing and fixing memory problems. You will write streamlined apps that keep your users' computers running smoothly.
We are now ready to move on to the last feature of Swift that we will discuss before we get into the more artful side of computer programming called error handling.