Home iOS & Swift Books Swift Apprentice

13
Classes Written by Cosmin Pupăză

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 really 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 14, “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.

class SimplePerson {
  let name: String
  init(name: String) {
    self.name = name
  }
}

var var1 = SimplePerson(name: "John")

var var2 = var1

struct SimplePerson {
  let name: String
}

The heap vs. the stack

When you create a reference type such as class, the system stores the actual instance in a region of memory known as the heap. Instances of a value type such as a struct resides in a region of memory called the stack, unless the value is part of a class instance, in which case the value is stored on the heap with the rest of the class instance.

Working with references

In Chapter 10, “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
var homeOwner = john
john.firstName = "John" // John wants to use his short name!
john.firstName // "John"
homeOwner.firstName // "John"

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?

john === homeOwner // true
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
// 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
// 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

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.

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")
let 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)

Mutability and constants

The previous example may have had you wondering how you were able to modify jane even though it was defined as a constant. When you define a constant, the value of the constant cannot be changed. If you recall back to the discussion of value types vs reference types, it’s important to remember that, with reference types, the value is a reference.

// Error: jane is a `let` constant
jane = Student(firstName: "John", lastName: "Appleseed")
var jane = Student(firstName: "Jane", lastName: "Appleseed")
jane = Student(firstName: "John", lastName: "Appleseed")

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).

Understanding state and side effects

Since the very nature of classes is that they are both referenced and mutable, there are many possibilities — as well as many concerns for programmers. Remember: If you update a class instance with a new value every reference to that instance will also see the new value.

var credits = 0.0
func recordGrade(_ grade: Grade) {
  grades.append(grade)
  credits += grade.credits
}
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!

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)"
  }
}

When to use a class versus a struct

Now that you know the differences and similarities between a class and a struct, you may be wondering “How do I know which to use?”

Values vs. objects

While there are no hard-and-fast rules, so you should think about value versus reference semantics, and use structures as values and classes as objects with identity.

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 much overhead.

Minimalist approach

Another approach is to use only what you need. If your data will never change or you need a simple data store, then use structures. If you need to update your data and you need it to contain logic to update its own state, then use classes. Often, it’s best to begin with a struct. If you need the added capabilities of a class sometime later, then you just 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 is best if you try to 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.

Challenge 2: T-shirt store

Your challenge here is to build a set of objects to support a T-shirt store. Decide if each object should be a class or a struct, and why.

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.