14.
Classes
Written by Eli Ganim
Structures introduced you to named types. In this chapter, you’ll get acquainted with classes, which are much like structures — they are named types with properties and methods.
You’ll learn classes are reference types, as opposed to value types, and have substantially different capabilities and benefits than their structure counterparts. While you’ll often use structures in your apps to represent values, you’ll generally use classes to represent objects.
What does values vs. objects mean, though?
Creating Classes
Consider the following class definition in Swift:
class Person {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
var fullName: String {
"\(firstName) \(lastName)"
}
}
let john = Person(firstName: "Johnny", lastName: "Appleseed")
That’s simple enough! It may surprise you that the definition is almost identical to its struct counterpart. The keyword class is followed by the name of the class, and everything in the curly braces is a member of that class.
But you can also see some differences between a class and a struct: The class above defines an initializer that sets both firstName and lastName to initial values. Unlike a struct, a class doesn’t provide a memberwise initializer automatically — which means you must provide it yourself if you need it. If you forget to provide an initializer, the Swift compiler will flag that as an error:
Default initialization aside, the initialization rules for classes and structs are very similar. Class initializers are functions marked init, and all stored properties must be assigned initial values before the end of init.
There is much more to class initialization, but you’ll have to wait until Chapter 15, “Advanced Classes”, which will introduce the concept of inheritance and its effect on initialization rules. This chapter will stick with basic class initializers so that you can get comfortable with classes in Swift.
Reference Types
In Swift, an instance of a structure is an immutable value, whereas an instance of a class is a mutable object. Classes are reference types, so a variable of a class type doesn’t store an actual instance — it stores a reference to a location in memory that stores the instance.
If you created a SimplePerson class instance with only a name like this:
class SimplePerson {
let name: String
init(name: String) {
self.name = name
}
}
var var1 = SimplePerson(name: "John")
It would look something like this in memory:
If you were to create a new variable var2 and assign to it the value of var1:
var var2 = var1
Then the references inside both var1 and var2 would reference the same place in memory:
Conversely, a structure as a value type stores the actual value, providing direct access to it. Replace the SimplePerson class implementation with a struct like this:
struct SimplePerson {
let name: String
}
The variable would not reference an external, shared place in memory but instead belong to var1 exclusively:
The assignment var var2 = var1 would copy the value of var1 in this case:
Value types and reference types each have their own distinct advantages — and disadvantages. Later in the chapter, you’ll consider which type to use in a given situation. You’ll now examine how classes and structs work under the hood.
The Heap vs. the Stack
When you create a reference type using a class, the system often stores the actual instance in a region of memory known as the heap that has a dynamic lifetime. Instances of value types typically reside in a region of memory called the stack that lives only as long as the current scope.
Both the heap and the stack have essential roles in the execution of any program. A general understanding of what they are and how they work will help you visualize the functional differences between a class and a structure:
-
The system uses the stack to store anything on the immediate thread of execution; it’s tightly managed and optimized by the CPU. A function allocates stack variables on entry and deallocates them on exit. Since the stack is so strictly organized, it’s very efficient.
-
The system uses the heap to store instances of reference types. The heap is generally a large pool of memory from which the system can request and dynamically allocate memory blocks. Heap variables’ lifetimes are flexible and dynamic.
The heap doesn’t automatically deallocate as the stack does; additional work is required. This extra work makes creating and removing data on the heap more involved.
You may have already figured out how this relates to structs and classes. Take a look at the diagram below:
-
When you create an instance of a class, your code requests a block of memory on the heap to store the instance itself; that’s the first name and last name inside the instance on the right side of the diagram. It stores the address of that memory in your named variable on the stack; that’s the reference stored on the left side of the diagram.
-
When you create an instance of a struct (that is not part of an instance of a class), the instance itself is stored on the stack, and the heap is never involved.
This essential mental model of heaps and stacks is enough to understand the reference semantics of classes. You’ll now get some additional experience working with them.
Working with References
In Chapter 11, “Structures”, you saw the copy semantics involved when working with structures and other value types. Here’s a little reminder, using the Location and DeliveryArea structures from that chapter:
struct Location {
let x: Int
let y: Int
}
struct DeliveryArea {
var range: Double
let center: Location
}
var area1 = DeliveryArea(range: 2.5,
center: Location(x: 2, y: 4))
var area2 = area1
print(area1.range) // 2.5
print(area2.range) // 2.5
area1.range = 4
print(area1.range) // 4.0
print(area2.range) // 2.5
When you assign the value of area1 into area2, area2 receives a copy of the area1 value. That way, when area1.range receives a new value of 4, the number is only reflected in area1 while area2 still has the original value of 2.5.
Since a class is a reference type, when you assign a class type variable, the system does not copy the instance; it only copies a reference.
Compare the previous code with the following code:
var homeOwner = john // "Johnny Appleseed"
john.firstName = "John" // John wants to use his short name!
john.firstName // "John"
homeOwner.firstName // "John"
As you can see, john and homeOwner truly have the same value!
This implied sharing among class instances results in a new way of thinking when passing things around. For instance, anything that references john will automatically see the update if the john object changes. If you were using a structure, you would have to update each copy individually, or it would still have the old value of “Johnny”.
Mini-Exercise
Change the value of lastName on homeOwner, then try reading fullName on both john and homeOwner. What do you observe?
Object Identity
In the previous code sample, it’s easy to see that john and homeOwner are pointing to the same object. The code is short, and both references are named variables. What if you want to see if the value behind a variable is John?
You might think to check the value of firstName, but how would you know it’s the John you’re looking for and not an imposter? Or worse, what if John changed his name again?
In Swift, the === operator lets you check if the identity of one object is equal to the identity of another:
john === homeOwner // true
Just as the == operator checks if two values are equal, the === identity operator compares the memory address of two references. It tells you whether the references are the same; that is, they point to the same block of data on the heap.
That means this === operator can tell the difference between the John you’re looking for and an imposter-John:
let imposterJohn = Person(firstName: "Johnny",
lastName: "Appleseed")
john === homeOwner // true
john === imposterJohn // false
imposterJohn === homeOwner // false
// Assignment of existing variables changes the instances the variables reference.
homeOwner = imposterJohn
john === homeOwner // false
homeOwner = john
john === homeOwner // true
This form of reference equality can be handy when you cannot rely on regular equality (==) to compare and identify objects you care about:
// Create fake, imposter Johns. Use === to see if any of these imposters are our real John.
var imposters = (0...100).map { _ in
Person(firstName: "John", lastName: "Appleseed")
}
// Equality (==) is not effective when John cannot be identified by his name alone
imposters.contains {
$0.firstName == john.firstName && $0.lastName == john.lastName
} // true
By using the identity operator, you can verify that the references themselves are equal and separate our real John from the crowd:
// Check to ensure the real John is not found among the imposters.
imposters.contains {
$0 === john
} // false
// Now hide the "real" John somewhere among the imposters.
imposters.insert(john, at: Int.random(in: 0..<100))
// John can now be found among the imposters.
imposters.contains {
$0 === john
} // true
// Since `Person` is a reference type, you can use === to grab the real John out of the list of imposters and modify the value.
// The original `john` variable will print the new last name!
if let indexOfJohn = imposters.firstIndex(where:
{ $0 === john }) {
imposters[indexOfJohn].lastName = "Bananapeel"
}
john.fullName // John Bananapeel
Because Swift emphasizes value types, you’ll find the reference identity operator === isn’t used that often. What’s important is to understand what it does and what it demonstrates about the properties of reference types.
Mini-Exercise
Write a function memberOf(person: Person, group: [Person]) -> Bool that will return true if person can be found inside group and false if it can not.
Test it by creating two arrays of five Person objects for group and using john as the person. Put john in one of the arrays but not in the other.
Methods and Mutability
As you’ve read before, instances of classes are mutable objects, whereas instances of structures are immutable values. The following example illustrates this difference:
struct Grade {
let letter: String
let points: Double
let credits: Double
}
class Student {
var firstName: String
var lastName: String
var grades: [Grade] = []
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
func recordGrade(_ grade: Grade) {
grades.append(grade)
}
}
let jane = Student(firstName: "Jane", lastName: "Appleseed")
var history = Grade(letter: "B", points: 9.0, credits: 3.0)
var math = Grade(letter: "A", points: 16.0, credits: 4.0)
jane.recordGrade(history)
jane.recordGrade(math)
Note that recordGrade(_:) can mutate the array grades by adding more values to the end. The keyword mutating is not required because it mutates the underlying object, not the reference itself.
If you had tried this with a struct, you’d get a compiler error because structure methods are, by default, immutable and can’t change any of their properties. The keyword mutating marks structure methods that can change stored properties. This keyword is not used with classes because a class is just a reference to some storage that another client could share and mutate. It would give you a false sense of security about a guarantee that doesn’t exist for methods not marked mutating.
Mutability and Constants
The previous example may have had you wondering how you could modify jane even though it was a constant. After all, when you define a constant, it doesn’t change. If you recall the discussion of value types vs. reference types, it’s important to remember that, with reference types, the value is a reference.
The value of “reference1” in red is the value stored in jane. This value is a reference, and because jane is declared a constant, this reference is constant. If you were to attempt to assign another student to jane, you would get a compiler error.
If you declared jane as a variable instead, you would be able to assign to it another instance of Student on the heap:
var jane = Student(firstName: "Jane", lastName: "Appleseed")
jane = Student(firstName: "John", lastName: "Appleseed")
After assigning another Student to jane, the reference value behind jane would be updated to point to the new Student object.
Since nothing would be referencing the original “Jane” object, its memory would be freed to use elsewhere.
Any individual member of a class can be protected from modification through the use of constants. Still, because reference types are not treated as values, they are not protected as a whole from mutation.
Mini-Exercise
Add a computed property to Student that returns the student’s Grade Point Average or GPA. A GPA is defined as the number of points earned divided by the number of credits taken. For the example above, Jane earned (9 + 16 = 25) points while taking (3 + 4 = 7) credits, making her GPA (25 / 7 = 3.57).
Note: Points in most American universities range from 4 per credit for an A, down to 1 point for a D (with an F being 0 points). For this exercise, you may, of course, use any scale that you want!
Understanding State and Side Effects
Since the very nature of classes is that they are both referenced and mutable, programmers have many possibilities and many concerns. Remember: If you update a class instance with a new value, every reference to that instance will also see the new value.
You can use this to your advantage. Perhaps you pass a Student instance to a sports team, a report card and a class roster. Imagine all of these entities need to know the student’s grades, and because they all point to the same instance, they’ll all see new grades as the instance records them.
The result of this sharing is that class instances have state. State changes can sometimes be obvious, but often they’re not.
To illustrate this, add a credits property to the Student class.
var credits = 0.0
and update recordGrade(_:) to use this new property:
func recordGrade(_ grade: Grade) {
grades.append(grade)
credits += grade.credits
}
In this slightly modified example of Student, recordGrade(_:) now adds the number of credits to the credits property. Calling recordGrade(_:) has the side effect of updating credits.
Now, observe how side effects can result in non-obvious behavior:
jane.credits // 7
// The teacher made a mistake; math has 5 credits
math = Grade(letter: "A", points: 20.0, credits: 5.0)
jane.recordGrade(math)
jane.credits // 12, not 8!
Whoever wrote the modified Student class did so somewhat naïvely by assuming that the same grade won’t get recorded twice!
Because class instances are mutable, you need to be careful about unexpected behavior around shared references.
While confusing in a small example, mutability and state could be highly jarring as classes grow in size and complexity.
Situations like this would be much more common as the Student class grows to include additional properties and methods.
Extending a Class Using an Extension
As you saw with structs, classes can be re-opened using the extension keyword to add methods and computed properties. Add a fullName computed property to Student:
extension Student {
var fullName: String {
"\(firstName) \(lastName)"
}
}
Functionality can also be added to classes using inheritance. You can even add new stored properties to inheriting classes. In Chapter 15, “Advanced Classes”, you’ll explore this technique in detail.
When to Use a Class Versus a Struct
You may wonder when to use a class vs. a struct. Here are some general guidelines.
Values vs. Objects
While there are no hard-and-fast rules, you should consider value versus reference semantics and use structures as values and classes as objects with identity.
An object is an instance of a reference type, and such instances have identity, meaning that every object is unique. Two objects may not be equal simply because they hold the same state. Hence, you use === to see if objects refer to the same place in memory. In contrast, instances of value types, which are values, are considered equal if they are the same value.
For example: A delivery range is a value, so you implement it as a struct. A student is an object, so you implement it as a class. In non-technical terms, no two students are equal, even if they have the same name!
Speed
Speed considerations are a thing, as structs rely on the faster stack while classes rely on the slower heap. If you’ll have many more instances (hundreds and greater), or if these instances will only exist in memory for a short time — lean towards using a struct. If your instance will have a longer lifecycle in memory, or if you’ll create relatively few instances, then class instances on the heap shouldn’t create too much overhead.
For example, you’d use a struct to calculate the total distance of a running route using many GPS-based waypoints, such as the Location struct you used in Chapter 11, “Structures”. You’ll create many waypoints, but they’ll be quickly created and destroyed as you modify the route.
You could also use a class for an object to store route history, as there would be only one object for each user, and you’d likely use the same history object for the user’s lifetime.
Minimalist Approach
Another approach is to use only what you need. Use structures if your data will never change or you need a simple data store. If you need to update your data and it contains logic to update its state, then use a class. Often, it’s best to begin with a struct. If you need the behavior of a class sometime later, you can convert the struct to a class.
Structures vs. Classes Recap
Structures
- Useful for representing values.
- Implicit copying of values.
- Becomes completely immutable when declared with
let. - Fast memory allocation (stack).
Classes
- Useful for representing objects with an identity.
- Implicit sharing of objects.
- Internals can remain mutable even when declared with
let. - Slower memory allocation (heap).
Challenges
Before moving on, here are some challenges to test your knowledge of classes. It’s best to try and solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Movie Lists
Imagine you’re writing a movie-viewing app in Swift. Users can create lists of movies and share those lists with other users. Create a User and a List class that uses reference semantics to help maintain lists between users.
-
User: Has a methodaddList(_:)that adds the given list to a dictionary ofListobjects (using thenameas a key), andlist(forName:) -> List?that returns theListfor the provided name. -
List: Contains a name and an array of movie titles. Areportmethod will print all the movies in the list. - Create
janeandjohnusers and create a list that they share. Have bothjaneandjohnmodify the list and callreportfrom both users. Are all the changes reflected? - What happens when you implement the same with structs? What problems do you run into?
Challenge 2: T-shirt Store
Your challenge here is to build a set of entities to support a T-shirt store. Decide if each entity should be a class or a struct and why.
-
TShirt: Represents a shirt style you can buy. EachTShirthas a size, color, price, and an optional image on the front. -
User: A registered user of the t-shirt store app. A user has a name, email, and aShoppingCart(see below). -
Address: This represents a shipping address and contains the name, street, city, and zip code. -
ShoppingCart: Holds a current order, composed of an array ofTShirtthat theUserwants to buy, as well as a method to calculate the total cost. Additionally, anAddressrepresents where the order will be shipped.
Bonus: After you’ve decided on whether to use a class or struct for each entity, go ahead and implement them all!
Key Points
- Like structures, classes are a named type that can have properties and methods.
- Classes use references that are shared on assignment.
- Class instances are called objects.
- Objects are mutable.
- Mutability introduces state, which adds complexity when managing your objects.
- Use classes when you want reference semantics; structures for value semantics.