Chapter 14: Advanced Classes

大綱

Introducing inheritance

  • 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

    • A chain of subclasses is called a class hierarchy

    • A superclass is also called the parent class of its child class.

class Person {
  var firstName: String
  var lastName: String

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

// the Student class now inherits from Person, indicated by a colon after the naming of Student
class Student: Person {
  var partner: Student?
  var grades: [Grade] = []

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

Polymorphism

  • polymorphism is a programming language’s ability to treat an object differently based on context.

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

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

phonebookName(person) // Appleseed, Johnny
// OboePlayer也是一個person
phonebookName(oboePlayer) // Appleseed, Jane

Runtime hierarchy checks

  • Swift provides the as operator to treat a property or a variable as another type:

    • as: Cast to a specific type that is known at compile time to succeed, such as casting to a supertype.

    • as?: An optional downcast (to a subtype). If the downcast fails, the result of the expression will be nil.

    • as!: A forced downcast. If the downcast fails, the program will crash. Use this rarely, and only when you are certain the cast will never fail.”

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)
  • The optional downcast as? is particularly useful in if let or guard statements

if let hallMonitor = hallMonitor as? BandMember {
  print("This hall monitor is a band member and practices at least \(hallMonitor.minimumPracticeTime) hours per week.")
}

Inheritance, methods and overrides

  • Besides creating their own methods, subclasses can override methods defined in their superclass

  • If your subclass were to have an identical method declaration as its superclass, but you omitted the override keyword, Swift would emit a compiler error

Introducing super

  • The super keyword is similar to self, except it will invoke the method in the nearest implementing superclass

When to call super

  • It’s best practice to call the super version of a method first when overriding. That way, the superclass won’t experience any side effects introduced by its subclas

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

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

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

Preventing inheritance

  • By marking the FinalStudent class final, you tell the compiler to prevent any classes from inheriting.

final class FinalStudent: Person {}
class FinalStudentAthlete: FinalStudent {} // Build error!
  • can mark individual methods as final, if you want to allow a class to have subclasses, but protect individual methods from being overridden:

class AnotherStudent: Person {
  final func recordGrade(_ grade: Grade) {}
}

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

Inheritance and class initialization

  • Initializers in subclasses are required to call super.init because without it, the superclass won’t be able to provide initial states for all its stored properties

Two-phase initialization

  • two-phase initialization.

    • Phase one: Initialize all of the stored properties in the class instance, from the bottom to the top of the class hierarchy. You can’t use properties and methods until phase one is complete.

    • Phase two: You can now use properties and methods, as well as initializations that require the use of self.

class StudentAthlete: Student {
  var sports: [String]

  init(firstName: String, lastName: String, sports: [String]) {
    // 1: initialize the sports property of StudentAthlete
    self.sports = sports
    // 2: create local variables for things like grades, you can’t call recordGrade(_:) yet because the object is still in the first phase
    let passGrade = Grade(letter: "P", points: 0.0, 
                          credits: 0.0)
    // 3: call super.init. When this returns, you know that you’ve also initialized every class in the hierarchy
    super.init(firstName: firstName, lastName: lastName)
    // 4: After super.init returns, the initializer is in phase 2, so you call recordGrade(_:)
    recordGrade(passGrade)
  }
  // original code
}

Required and convenience initializers

  • it’s possible to have multiple initializers in a class

  • Swift supports this through the language feature known as required initializers

    • This keyword will force all subclasses of Student to implement this initializer.

  • You can also mark an initializer as a convenience initializer

  • A non-convenience initializer is called a designated initializer and is subject to the rules of two-phase initialization.

class NewStudentAthlete: NewStudent {
  var failedClasses: [Grade] = []
  var sports: [String]
  // designated initializer
  init(firstName: String, lastName: String, sports: [String]) {
    self.sports = sports
    let passGrade = Grade(letter: "P", points: 0.0, credits: 0.0)
    super.init(firstName: firstName, lastName: lastName)
    recordGrade(passGrade)
  }
  // required / designated initializers
  required init(firstName: String, lastName: String) {
    self.sports = []
    super.init(firstName: firstName, lastName: lastName)
  }

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

    if grade.letter == "F" {
      failedClasses.append(grade)
    }
  }
  // convenience initializer
  convenience init(transfer: NewStudent) {
    self.init(firstName: transfer.firstName, lastName: transfer.lastName)
  }

  var isEligible: Bool {
    return failedClasses.count < 3
  }
}
  • Here’s a summary of the compiler rules for using designated and convenience initializers:

    • A designated initializer must call a designated initializer from its immediate superclass.

    • A convenience initializer must call another initializer from the same class.

    • A convenience initializer must ultimately call a designated initializer.

When and why to subclass

Single responsibility

  • single responsibility principle states that any class should have a single concern.

Strong types

  • Subclassing creates an additional type

class Team {
  // A team has players who are student athletes. If you tried to add a regular Student object to the array of players, the type system wouldn’t allow it
  var players: [StudentAthlete] = []

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

Shared base classes

  • It makes sense for Button to be concerned with the press behavior, and the subclasses to handle the actual look and feel of the button.

// 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

  • you can subclass Button and add your custom subclass to use with code that’s expecting an object of type Button.

Identity

  • 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

Understanding the class lifecycle

  • In Swift, the mechanism for deciding when to clean up unused objects on the heap is known as reference counting.

  • When a reference count reaches zero, that means the object is now abandoned since nothing in the system holds a reference to it. When that happens, Swift will clean up the object.

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

  • A deinitializer is a special method on classes that runs when an object’s reference count reaches zero, but before Swift removes the object from memory.

    • What you do in an deinitializer is up to you. Often you’ll use it to clean up other resources, save state to a disk or execute any other logic you might want when an object goes out of scope

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

Retain cycles and weak references

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

// 造成Retain cycles
alice?.partner = bob
bob?.partner = alice

// 即使都變成nil, 也無法release
alice = nil
bob = nil
class Student: Person {
  // 利用weak來處理Retain cycles
  // Weak references must be declared as optional types so that when the object that they are referencing is released, it automatically becomes nil
Excerpt From: By Ray Fix. “Swift Apprentice.” Apple Books. 
  weak var partner: Student?
  // original code
}

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.

Last updated

Was this helpful?