Leave a rating/review
Notes: 38. Inheritance
Update Notes: This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.
In the Swift Fundamentals course you learned some distinguishing characteristics between classes and structures: classes are reference types, and structures are value types.
In this episode we’ll talk about the main feature of classes that structures and enumerations don’t have: inheritance. Let’s see it in action first, and then we’ll go over the terminology.
You can find this code we’re starting off with in the playground for this part of the course.You’ve got a grade, which is a structure, and two classes, to represent two types of people: those who are students, and those who are not.
But while not every Person is a student, every student is a Person. Aside from those words being true, in the real world, you can see the idea modeled here in code, as well. Student has all the code that Person does, and an in addition, an Array of Grades.
With classes, there’s a way to represent this idea in code, where one type is a more specific version of another type. You do that by putting a colon after the more specific type’s name, and then using the name of the more general type.
48 class Student: Person {
You’ll come to understand exactly what these errors mean later, but for now, you can consider them to be marking where you’re repeating yourself. Because a Student is a Person now, Student has access to the properties and initializer defined within Person.
So, you can delete the repeated code.
48 class Student: Person {
var grades: [Grade] = []
}
Now you’re only defining what’s special about a Student: their grades. Let’s see how that works, using some class instances. You can still create a Person, using the initializer defined within Person:
52 let jon = Person(firstName: "Jon", lastName: "Snon")
But you can also create a Student with it. You just need to use the type name Student instead of Person.
53 let jane = Student(firstName: "Jane", lastName: "Snane")
Students will have access to all the properties and methods of Person.
55 jon.firstName
jane.firstName
And, they’ll of course have access to their own grades.
let historyGrade = Grade(letter: "B", points: 9, credits: 3)
jane.grades.append(historyGrade)
But a Person will not have access to any grades, because they are not a student.
61 jon.grades
You’ve now learned the basics of class inheritance. For the example we coded, we’d say that “Student inherits from Person”. Also, we can say that, “Student is a subclass of Person”. And “Person is the superclass of Student”.
Now that you’ve said that Student inherits from Person, you can’t say that it inherits from any other class. In Swift, a class has two options: It can inherit from 1 superclass…
…Or, it can not inherit. In that case, it’s called a “base class”. In this exercise, Person is a base class.
You could think of other types of people than Students. If you needed to model them in your app, you could have them inherit from Person as well. A superclass, such as Person, can have as many subclasses as necessary.
Lastly, you can subclass from a subclass. There’s no limit to the depth of how far you can go with that.
Let’s explore those last two concepts.
Define a new class –SchoolBandMember– that inherits from Student.
61 class SchoolBandMember: Student {
}
In addition to a name, and grades, every band member will need to have a minimum practice time. That can be a stored property.
62 var minimumPracticeTime = 2
Now, define another class that inherits from Student: StudentAthlete.
class StudentAthlete: Student {
}
A student athlete won’t be eligible if they get three Fs. We can express that with a computed property.
class StudentAthlete: Student {
66 var isEligible: Bool {
return grades.filter { $0.letter == "F" } .count < 3
}
}
Now we’ve got one base class: Person. One subclass of it: Student. and two subclasses of Student.
A chain of subclasses, like this, is called a “class hierarchy”. A class hierarchy is analogous to a family tree. And because of that, you’ll sometimes hear a superclass being referred to as the “parent class” of its subclasses. And the subclasses are “children”.
In our example class hierarchy, SchoolBandMembers and StudentAthletes, are Students. Both conceptually, and to the Swift compiler. They’re also Persons, as well.
Because of that, anywhere in your code that need a Person object, you could use any type of Student. This capability demonstrates a concept called polymorphism. That means, depending on context, a subclass can be treated as its own type, or, as one of its superclasses.
Let’s define a band member and student athlete, and then we can play with polymorphism. Jessy is a guitar player.
75 let jessy = SchoolBandMember(firstName: "Jessy", lastName: "Catterwaul")
And let’s imagine he’s at school with an athlete named “Marty McWolf”.
let marty = StudentAthlete(firstName: "Marty", lastName: "McWolf")
Now, let’s take all four of the people we’ve defined so far, and put them into an array.
74 let array = [jon, jane, jessy, marty]
If we check the implicit type of the array…
…we’ll find that the compiler is smart enough to know that it’s a Person Array. Even though we’ve a Person, Student, SchoolBandMember, and StudentAthlete, because they inherit from a base class, or are an instance of the base class (in the case of Jon), they can all be put an array.
But now, let’s an explicit type to the array, so it can only contain Students.
74 let array: [Student] = …
Now, we’ve got a problem. Jon is not a student, so it doesn’t make sense for him to be in the array. Swift won’t allow it. So, get him out of there.
74 … = [jane, jessy, marty]
And of course, you could delete the explicit type, and it would still be a Student Array.
let array =
Swift will find the most specific, but still common superclass, if it exists, for everything in your array. nother way to put this, if you’re interested, is that Swift’s Array type is covariant. But you don’t need to know the jargon – the way polymorphism in Swift works is pretty intuitive.
Aside from explicit typing, you can also cast from a subclass to a superclass. Casting uses the keyword as, followed by a type to cast to.
76 let student = marty as Student
That’s called “upcasting”. “Downcasting” goes the other way: you take a superclass instance, and turn it into a subclass instance.
77 let athlete = student as! StudentAthlete
For downcasting, you’ve two options. One of them is using this exclamation point. And from what you know about optionals, you might guess the name of this technique: “forced downcasting”. But only use it if you’re 100% sure that the cast will succeed; otherwise your app will crash.
77 … student as! SchoolBandMember
Marty is not a band member, so we can’t force cast him to be one.
And here, that’s very clear. But sometimes, in an app, you won’t know if a certain cast will succeed. Let’s illustrate that in a function that takes a Student and returns a String.
79 func getEveningActivity(student: Student) -> String {
}
If we want to return a special value, when the student is a SchoolBandMember, then we can use the conditional downcast operator, which returns nil when the cast fails. It works really well for “if let” or “guard let” statements:
80 if let bandMember = student as? SchoolBandMember {
} else {
return "Hitting the books!"
}
When the cast succeeds, we’ll have access to the bandMember’s minimumPracticeTime.
81 return "Practicing for at least \(bandMember.minimumPracticeTime) hours."
Calling the function with both a SchoolBandMember, and a Student, shows off another was to use polymorphism.
getEveningActivity(student: jessy)
getEveningActivity(student: jane)
Besides defining all-new methods and properties, subclasses can override methods and properties defined in their superclasses. Our student athletes become ineligible for the athletics program if they’re failing three or more classes. So, perhaps it makes sense to take special action when tracking their grades.
We’ll be needing the override keyword for what we’re about to do, but it’s easiest if you start typing the name of the member you’ll be overriding. In this, for StudentAthlete, that’s grades.
70 gr
If you let that autocomplete, you’ll get a repeat of the original definition, but with the override keyword preceding it.
70 override var grades: [Grade]
Then, you can do some overriding. In this case, we’ll start by making grades into a computed property. Making that switch is perfectly fine. (Though it only works one way; you can’t override a computed property with a stored property.)
70 override var grades: [Grade] {
get { }
set {
}
}
The return value, for “get”, can be the grades that every student would store. That value is accessed as “super.grades”.
get { return super.grades }
super gives you everything that’s available to a Student, but not specifically to StudentAthlete. And for set, you should assign the newValue to super.grades, so they get stored.
set {
73 super.grades = newValue
}
When new grades are assigned, if the student athlete has too many “F”s, let’s print out a call to go study, and fix that situation.
74 if !isEligible {
print("It's time to study!")
}
To test that out, create a grade that represents a failure. For this exercise, that’s anything with the letter F.
89 let utterFailureGrade = Grade(letter: "F", points: 0, credits: 0)
Now, it’s time to be stern. Give one of those grades to this student athlete, who has never shown up to class.
90 athlete.grades.append(utterFailureGrade)
…Or, that other class.
91 athlete.grades.append(utterFailureGrade)
And they actually went to the third one, which was on Programming in Swift! But their dog ate their playground.
92 athlete.grades.append(utterFailureGrade)
And so, it’s studyin’ time! Although this is a valid way to code this behavior, if you’re not doing anything else with the getter, other than getting the value from super, there’s a shorter way to express the same code.
You can delete the get accessor and the assignment to super.grades, and instead, use a property observer.
70 override var grades: [Grade] {
didSet {
if !isEligible {
print("It's time to study!")
}
}
}
You can also override methods, but that’s even simpler, because you don’t have to have to worry about whether to use a computed property, or property observers. However, inheriting initializers is more complex. We’ll try to shed some light on that in the next exercise!