Home iOS & Swift Books Swift Apprentice

14
Advanced Classes Written by Cosmin Pupăză

The previous chapter introduced you to the basics of defining and using classes in Swift. Classes are reference types and can be used to support traditional object-oriented programming.

Classes introduce inheritance, overriding, polymorphism which makes them suited for this purpose. These extra features require special consideration for initialization, class hierarchies, and understanding the class lifecycle in memory.

This chapter will introduce you to the finer points of classes in Swift and help you understand how you can create more complex classes.

Introducing inheritance

In the previous chapter, you saw a Grade struct and a pair of class examples: Person and Student.

struct Grade {
  var letter: Character
  var points: Double
  var credits: Double
}

class Person {
  var firstName: String
  var lastName: String

  init(firstName: String, lastName: String) {
    self.firstName = firstName
    self.lastName = lastName
  }
}

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

It’s not difficult to see that there’s redundancy between Person and Student. Maybe you’ve also noticed that a Student is a Person!

This simple case demonstrates the idea behind class inheritance. Much like in the real world, where you can think of a student as a person, you can represent the same relationship in code by replacing the original Student class implementation with the following:

class Student: Person {
  var grades: [Grade] = []

  func recordGrade(_ grade: Grade) {
    grades.append(grade)
  }
}

In this modified example, the Student class now inherits from Person, indicated by a colon after the naming of Student, followed by the class from which Student inherits, which in this case is Person.

Through inheritance, Student automatically gets the properties and methods declared in the Person class. In code, it would be accurate to say that a Student is-a Person.

With much less duplication of code, you can now create Student objects that have all the properties and methods of a Person:

let john = Person(firstName: "Johnny", lastName: "Appleseed")
let jane = Student(firstName: "Jane", lastName: "Appleseed")

john.firstName // "John"
jane.firstName // "Jane"

Additionally, only the Student object will have all of the properties and methods defined in Student:

let history = Grade(letter: "B", points: 9.0, credits: 3.0)
jane.recordGrade(history)
// john.recordGrade(history) // john is not a student!

A class that inherits from another class is known as a subclass or a derived class, and the class from which it inherits is known as a superclass or a base class.

The rules for subclassing are fairly simple:

  • A Swift class can inherit from only one other class, a concept known as single inheritance.
  • There’s no limit to the depth of subclassing, meaning you can subclass from a class that is also a subclass, like below:
class BandMember: Student {
  var minimumPracticeTime = 2
}

class OboePlayer: BandMember {
  // This is an example of an override, which we’ll cover soon.
  override var minimumPracticeTime: Int {
    get {
      super.minimumPracticeTime * 2
    }
    set {
      super.minimumPracticeTime = newValue / 2
    }
  }
}

A chain of subclasses is called a class hierarchy. In this example, the hierarchy would be OboePlayer -> BandMember -> Student -> Person. A class hierarchy is analogous to a family tree. Because of this analogy, a superclass is also called the parent class of its child class.

Polymorphism

The Student/Person relationship demonstrates a computer science concept known as polymorphism. In brief, polymorphism is a programming language’s ability to treat an object differently based on context.

func phonebookName(_ person: Person) -> String {
  "\(person.lastName), \(person.firstName)"
}

let person = Person(firstName: "Johnny", lastName: "Appleseed")
let oboePlayer = OboePlayer(firstName: "Jane",
                            lastName: "Appleseed")

phonebookName(person) // Appleseed, Johnny
phonebookName(oboePlayer) // Appleseed, Jane

Runtime hierarchy checks

Now that you are coding with polymorphism, you’ll likely find situations where the specific type behind a variable can be different. For instance, you could define a variable hallMonitor as a Student:

var hallMonitor = Student(firstName: "Jill", 
                          lastName: "Bananapeel")
hallMonitor = oboePlayer
oboePlayer as Student
(oboePlayer as Student).minimumPracticeTime // ERROR: No longer a band member!

hallMonitor as? BandMember
(hallMonitor as? BandMember)?.minimumPracticeTime // 4 (optional)

hallMonitor as! BandMember // Careful! Failure would lead to a runtime crash.
(hallMonitor as! BandMember).minimumPracticeTime // 4 (force unwrapped)
if let hallMonitor = hallMonitor as? BandMember {
  print("This hall monitor is a band member and practices 
         at least \(hallMonitor.minimumPracticeTime) 
         hours per week.")
}
func afterClassActivity(for student: Student) -> String {
  "Goes home!"
}

func afterClassActivity(for student: BandMember) -> String {
  "Goes to practice!"
}
afterClassActivity(for: oboePlayer) // Goes to practice!
afterClassActivity(for: oboePlayer as Student) // Goes home!

Inheritance, methods and overrides

Subclasses receive all properties and methods defined in their superclass, plus any additional properties and methods the subclass defines for itself. In that sense, subclasses are additive.

class StudentAthlete: Student {
  var failedClasses: [Grade] = []

  override func recordGrade(_ grade: Grade) {
    super.recordGrade(grade)

    if grade.letter == "F" {
      failedClasses.append(grade)
    }
  }

  var isEligible: Bool {
    failedClasses.count < 3
  }
}

Introducing super

You may have also noticed the line super.recordGrade(grade) in the overridden method. The super keyword is similar to self, except it will invoke the method in the nearest implementing superclass. In the example of recordGrade(_:) in StudentAthlete, calling super.recordGrade(grade) will execute the method as defined in the Student class.

When to call super

As you may notice, exactly when you call super can have an important effect on your overridden method.

override func recordGrade(_ grade: Grade) {
  var newFailedClasses: [Grade] = []
  for grade in grades {
    if grade.letter == "F" {
      newFailedClasses.append(grade)
    }
  }
  failedClasses = newFailedClasses

  super.recordGrade(grade)
}

Preventing inheritance

Sometimes you’ll want to disallow subclasses of a particular class. Swift provides the final keyword for you to guarantee a class will never get a subclass:

final class FinalStudent: Person {}
class FinalStudentAthlete: FinalStudent {} // Build error!
class AnotherStudent: Person {
  final func recordGrade(_ grade: Grade) {}
}

class AnotherStudentAthlete: AnotherStudent {
  override func recordGrade(_ grade: Grade) {} // Build error!
}

Inheritance and class initialization

The previous chapter briefly introduced you to class initializers, which are similar to their struct counterparts. With subclasses, there are a few more considerations about how you set up instances.

class StudentAthlete: Student {
  var sports: [String]
  // original code
}
class StudentAthlete: Student {
  var sports: [String]

  init(sports: [String]) {
    self.sports = sports
    // Build error - super.init isn’t called before
    // returning from initializer
  }
  // original code
}

class StudentAthlete: Student {
  var sports: [String]

  init(firstName: String, lastName: String, sports: [String]) {
    self.sports = sports
    super.init(firstName: firstName, lastName: lastName)
  }
  // original code
}

Two-phase initialization

Because of Swift’s requirement that all stored properties have initial values, initializers in subclasses must adhere to Swift’s convention of two-phase initialization.

class StudentAthlete: Student {
  var sports: [String]

  init(firstName: String, lastName: String, sports: [String]) {
    // 1
    self.sports = sports
    // 2
    let passGrade = Grade(letter: "P", points: 0.0, 
                          credits: 0.0)
    // 3
    super.init(firstName: firstName, lastName: lastName)
    // 4
    recordGrade(passGrade)
  }
  // original code
}

Mini-exercise

What’s different in the two-phase initialization in the base class Person, as compared to the others?

Required and convenience initializers

You already know it’s possible to have multiple initializers in a class, which means you could potentially call any of those initializers from a subclass.

class Student {
  let firstName: String
  let lastName: String
  var grades: [Grade] = []

  init(firstName: String, lastName: String) {
    self.firstName = firstName
    self.lastName = lastName
  }

  init(transfer: Student) {
    self.firstName = transfer.firstName
    self.lastName = transfer.lastName
  }

  func recordGrade(_ grade: Grade) {
    grades.append(grade)
  }
}
class Student {
  let firstName: String
  let lastName: String
  var grades: [Grade] = []

  required init(firstName: String, lastName: String) {
    self.firstName = firstName
    self.lastName = lastName
  }
  // original code
}
class StudentAthlete: Student {
  // Now required by the compiler!
  required init(firstName: String, lastName: String) {
    self.sports = []
    super.init(firstName: firstName, lastName: lastName)
  }
  // original code
}
class Student {
  convenience init(transfer: Student) {
    self.init(firstName: transfer.firstName, 
              lastName: transfer.lastName)
  }
  // original code
}

Mini-exercise

Create two more convenience initializers on Student. Which other initializers are you able to call?

When and why to subclass

This chapter has introduced you to class inheritance, along with the numerous programming techniques that subclassing enables.

class Student: Person {
  var grades: [Grade]
  var sports: [Sport]
  // original code
}

Single responsibility

In software development, the guideline known as the single responsibility principle states that any class should have a single concern. In Student/StudentAthlete, you might argue that it shouldn’t be the Student class’s job to encapsulate responsibilities that only make sense to student-athletes.

Strong types

Subclassing creates an additional type. With Swift’s type system, you can declare properties or behavior based on objects that are student-athletes, not regular students:

class Team {
  var players: [StudentAthlete] = []

  var isEligible: Bool {
    for player in players {
      if !player.isEligible {
        return false
      }
    }
    return true
  }
}

Shared base classes

You can subclass a shared base class multiple times by classes that have mutually exclusive behavior:

// A button that can be pressed.
class Button {
  func press() {}
}

// An image that can be rendered on a button
class Image {}

// A button that is composed entirely of an image.
class ImageButton: Button {
  var image: Image

  init(image: Image) {
    self.image = image
  }
}

// A button that renders as text.
class TextButton: Button {
  var text: String

  init(text: String) {
    self.text = text
  }
}

Extensibility

Sometimes you need to extend the behavior of code you don’t own. In the example above, it’s possible Button is part of a framework you’re using, so there’s no way you can modify or extend the source code to fit your specific case.

Identity

Finally, it’s important to understand that classes and class hierarchies model what objects are. If your goal is to share behavior (what objects can do) between types, more often than not you should prefer protocols over subclassing. You’ll learn about protocols in Chapter 16, “Protocols”.

Understanding the class lifecycle

In the previous chapter, you learned that objects are created in memory and that they’re stored on the heap. Objects on the heap are not automatically destroyed because the heap is simply a giant pool of memory. Without the utility of the call stack, there’s no automatic way for a process to know that a piece of memory will no longer be in use.

var someone = Person(firstName: "Johnny", lastName: "Appleseed")
// Person object has a reference count of 1 (someone variable)

var anotherSomeone: Person? = someone
// Reference count 2 (someone, anotherSomeone)

var lotsOfPeople = [someone, someone, anotherSomeone, someone]
// Reference count 6 (someone, anotherSomeone, 4 references in lotsOfPeople)

anotherSomeone = nil
// Reference count 5 (someone, 4 references in lotsOfPeople)

lotsOfPeople = []
// Reference count 1 (someone)
someone = Person(firstName: "Johnny", lastName: "Appleseed")
// Reference count 0 for the original Person object!
// Variable someone now references a new object

Deinitialization

When an object’s reference count reaches zero, Swift removes the object from memory and marks that memory as free.

class Person {
  // original code
  deinit {
    print("\(firstName) \(lastName) is being removed
          from memory!")
  }
}

Mini-exercises

Modify the Student class to have the ability to record the student’s name to a list of graduates. Add the name of the student to the list when the object is deallocated.

Retain cycles and weak references

Because classes in Swift rely on reference counting to remove them from memory, it’s important to understand the concept of a retain cycle.

class Student: Person {
  var partner: Student?
  // original code
  deinit {
    print("\(firstName) is being deallocated!")
  }
}

var alice: Student? = Student(firstName: "Alice",
                              lastName: "Appleseed")
var bob: Student? = Student(firstName: "Bob",
                            lastName: "Appleseed")

alice?.partner = bob
bob?.partner = alice
alice = nil
bob = nil
class Student: Person {
  weak var partner: Student?
  // original code
}

Challenges

Before moving on, here are some challenges to test your knowledge of advanced 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: Initialization order

Create three simple classes called A, B, and C where C inherits from B and B inherits from A. In each class initializer, call print("I’m <X>!") both before and after super.init(). Create an instance of C called c. What order do you see each print() called in?

Challenge 2: Deinitialization order

Implement deinit for each class. Create your instance c inside of a do { } scope, causing the reference count to go to zero when it exits the scope. Which order do the classes deinitialize?

Challenge 3: Type casting

Cast the instance of type C to an instance of type A. Which casting operation do you use and why?

Challenge 4: To subclass or not

Create a subclass of StudentAthlete called StudentBaseballPlayer and include properties for position, number, and battingAverage. What are the benefits and drawbacks of subclassing StudentAthlete in this scenario?

Key points

  • Class inheritance is one of the most important features of classes and enables polymorphism.
  • Subclassing is a powerful tool, but it’s good to know when to subclass. Subclass when you want to extend an object and could benefit from an “is-a” relationship between subclass and superclass, but be mindful of the inherited state and deep class hierarchies.
  • The keyword override makes it clear when you are overriding a method in a subclass.
  • The keyword final can be used to prevent a class from being subclassed.
  • Swift classes use two-phase initialization as a safety measure to ensure all stored properties are initialized before they are used.
  • Class instances have their own lifecycles which are controlled by their reference counts.
  • Automatic reference counting, or ARC, handles reference counting for you automatically, but it’s important to watch out for retain cycles.

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.