Runtime hierarchy checks
Now that you are coding with polymorphism, you will 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")
But what if hallMonitor were a more derived type, such as an OboePlayer?
hallMonitor = oboePlayer
Because hallMonitor is defined as a Student, the compiler won’t allow you to attempt calling properties or methods for a more derived type.
Fortunately, 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.
These can be used in various contexts to treat the hallMonitor as a BandMember, or the oboePlayer as a less-derived Student.
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.")
}
You may be wondering under what contexts you would use the as operator by itself. Any object contains all the properties and methods of its more derived classes, so what use is casting it to something it already is?
Swift has a strong type system, and the interpretation of a specific type can have an effect on static dispatch, or the decision of which specific operation is selected at compile time.
Sound confusing? How about an example!
If you were to write two functions with identical names and parameter names for two different parameter types:
func afterClassActivity(for student: Student) -> String { return "Goes home!"
}
func afterClassActivity(for student: BandMember) -> String { return "Goes to practice!"
}
If you were to pass oboePlayer into afterClassActivity(for:), which one of these implementations would get called? The answer lies in Swift’s dispatch rules, which in this case will select the more-derived version that takes in an OboePlayer.
If instead you were to cast oboePlayer to a Student, the Student version would be called:
afterClassActivity(for: oboePlayer) // Goes to practice! afterClassActivity(for: oboePlayer as Student) // Goes home!