前一章介绍了快速定义和使用类的基本知识。类是引用类型,可以用来支持传统的面向对象编程。
类引入了继承、重写、多态性和组合。这些额外的特性需要考虑特殊的初始化、类层次结构和理解内存中的类生命周期。
这一章将向你介绍Swift中更复杂的类,并帮助你理解如何创建更复杂的类。
继承
在前一章中,你看到了一个Grade结构和两个类的例子:Person 和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)
}
}
不难看出,人与学生之间存在着很多相同的地方。你也许已经发现一个学生就是一个人!
这个简单的案例展示了类继承背后的思想。就像在现实世界中,你可以把学生看作是一个人,你可以用下面的方法来代替原来的学生类,表示代码中的相同关系:
class Student: Person {
var grades: [Grade] = []
func recordGrade(_ grade: Grade) {
grades.append(grade)
}
}
在这个修改的示例中,Student类现在继承自Person,在Student类名后加 : ,后面是Student继承的类,在这个例子中是Person。
通过继承,学生自动获取Person类中声明的属性和方法。在代码中,说一个学生是一个人是正确的
使用更少的代码,你就可以创建具有Person所有属性和方法的Student对象:
let john = Person(firstName: "Johnny", lastName: "Appleseed")
let jane = Student(firstName: "Jane", lastName: "Appleseed")
john.firstName // "John"
jane.firstName // "Jane"
另外,只有Student对象具有Student类中定义的所有的属性和方法。
let history = Grade(letter: "B", points: 9.0, credits: 3.0)
jane.recordGrade(history)
// john.recordGrade(history) // john is not a student!
从另一个类继承的类被称为子类或派生类,它继承的类被称为超类或基类。
子类化的规则相当简单:
•一个Swift类可以从另一个类继承,这个概念被称为single继承。
•对子类的深度没有限制,这意味着你可以从一个类中子类化。如下所示这也是一个子类:
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 {
return super.minimumPracticeTime * 2
}
set {
super.minimumPracticeTime = newValue / 2
}
} }
一串子类被称为类层次结构。在这个例子中,层次结构将是OboePlayer -> BandMember -> Student -> Person。类层次结构类似于家族树。由于这个类比,超类也被称为其子类的父类。
多态性
Student/Person 关系展示了一种被称为多态性的计算机科学概念。简言之,多态性是一种编程语言的能力,它可以根据上下文对对象进行不同的处理。
一个OboePlayer当然是一个OboePlayer,但它也是一个人。因为它源于Person,所以你可以在任何使用Person对象的地方使用OboePlayer对象。
这个例子演示了如何将一个OboePlayer视为一个人:
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
phonebookName(oboePlayer) // Appleseed, Jane
因为OboePlayer来自Person,所以它是函数phonebookName(_:)的有效输入。更重要的是,这个函数不知道传入的对象不是普通的人。它只知道OboePlayer元素是在Person基类中定义的。
基于类继承提供的多态性特征,Swift以不同的上下文对oboePlayer指向的对象进行处理。当你有不同的类层次结构时,并想操作公共类型或基类的代码的时候,这可能对你有用。
层次结构运行时检查
现在你正在使用多态性进行编码,你可能会发现变量后面的特定类型可能会有所不同。例如,你可以定义一个变量hallMonitor作为学生实例:
var hallMonitor = Student(firstName: "Jill",
lastName: "Bananapeel")
但是如果hallMonitor是一个更派生的类型,比如OboePlayer呢?
hallMonitor = oboePlayer
因为hallMonitor是作为一个学生定义的,所以编译器不允许你尝试调用属性或方法来获得更派生的类型。
幸运的是,Swift提供了将属性或变量作为另一种类型的操作符:
• as :转换到编译时已知的特定类型,例如转换为超类型。
• as? :可选的向下转换(到子类型)。如果向下转换失败,表达式的结果将为nil。
• as! :强制转换。如果向下转换失败,程序将崩溃。使用这个很少,只有当你确信转换永远不会失败。
这些可以在不同的上下文中使用,可以将hallMonitor转换为一个BandMember或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)
在使用let 和 guard语句中,可选的向下转换 as? 是很有用的。
if let hallMonitor = hallMonitor as? BandMember {
print("This hall monitor is a band member and practices at least \(hallMonitor.minimumPracticeTime)
hours per week.")
}
你可能想知道,在什么情况下,你将使用as操作符?
假设你有两个具有相同名称和参数名称的函数,用于两个不同的参数类型:
func afterClassActivity(for student: Student) -> String {
return "Goes home!"
}
func afterClassActivity(for student: BandMember) -> String {
return "Goes to practice!"
}
如果你将oboePlayer传递到afterClassActivity(for:),那么哪一个方法会被调用?答案就在Swift的调度规则中,在本例中,它将选择使用更具体的版本。
如果你把oboePlayer转化为Student,就会调用参数是Student类型的方法:
afterClassActivity(for: oboePlayer) // Goes to practice!
afterClassActivity(for: oboePlayer as Student) // Goes home!
继承,方法和重写
子类接收其超类中定义的所有属性和方法,加上子类定义的任何其他属性和方法。在这个意义上,子类是补充剂。例如,你已经看到Student类可以添加额外的属性和方法来处理学生的成绩。这些属性和方法不能用于任何Person类实例,但是它们可以用于Student子类。
除了创建自己的方法之外,子类还可以覆盖(重写)超类中定义的方法。假设学生运动员在比赛中失败三次或三次以上没有资格参加田径项目。这就意味着你需要知道不及格的成绩。
class StudentAthlete: Student {
var failedClasses: [Grade] = []
override func recordGrade(_ grade: Grade) {
super.recordGrade(grade)
if grade.letter == "F" {
failedClasses.append(grade)
}
}
var isEligible: Bool {
return failedClasses.count < 3
}
}
在这个例子中,StudentAthlete类覆盖了recordGrade(_:),这样它就可以跟踪学生失败的任何课程。StudentAthlete有自己的计算属性,也就是,使用这些信息来确定运动员的资格。
当重写方法时,在方法声明之前使用override关键字。
如果你的子类有一个相同的方法声明作为它的超类,但是你省略了override关键字,Swift会指出一个构建错误:
这样就可以很清楚地知道一个方法是否覆盖了现有的方法。
super
你可能还注意到覆盖方法中的super. recordgrade (grade)。super关键字与self类似,只是它将调用最近的实现的超类中的方法。在StudentAthlete的recordGrade(_:)的例子中,调用super.recordGrade(grade)将执行在Student类中定义的方法。
记住,继承如何让你定义具有姓氏和姓氏属性的人,并避免在子类中重复这些属性?同样,能够调用超类方法意味着你可以编写代码来记录学生的成绩,然后根据需要在子类中向上调用。
虽然它并不总是必需的,但是在Swift中重写方法时调用super是很重要的。super 调用是在grade数组中记录分数本身的内容,因为StudentAthlete(学生运动员)的行为并不是重复的。调用super也是避免StudentAthlete(学生运动员)和Student(学生)重复代码的一种方式。
何时调用super
你可能注意到的,当你调用super时,它对你覆盖的方法有重要的影响。
假设你在StudentAthlete(学生运动员)类中替换了overriden recordGrade(_:)方法,该方法使用以下版本,每次记录一个grade时重新计算failedClasses:
override func recordGrade(_ grade: Grade) {
var newFailedClasses: [Grade] = []
for grade in grades {
if grade.letter == "F" {
newFailedClasses.append(grade)
}
}
failedClasses = newFailedClasses
super.recordGrade(grade)
}
这个版本的recordGrade(_:)在grades数组中查找失败的成绩。既然你在最后调用super,如果是新的grade.letter等于F,会在此添加grade到failedClasses,代码就不能正确地更新failedClasses。
虽然这不是一个硬性规则,但通常最好是在重写时首先调用方法的super。这样,超类就不会体验到它的子类引入时产生的副作用,子类也不需要知道超类的实现细节。
防止继承
有时,你希望禁用某类的子类。Swift为你提供了final关键字,以保证类永远不会得到子类:
final class FinalStudent: Person {}
class FinalStudentAthlete: FinalStudent {} // Build error!
通过 final 标记FinalStudent类,你告诉编译器阻止任何类从FinalStudent继承。这可以提醒你——或者你的团队中的其他人!-这个类不是为有子类而设计的。
此外,如果你允许类具有子类,但是想保护单个方法不被重写或者覆盖,你可以将单个方法标记为final:
class AnotherStudent: Person {
final func recordGrade(_ grade: Grade) {}
}
class AnotherStudentAthlete: AnotherStudent {
override func recordGrade(_ grade: Grade) {} // Build error!
}
在开始给任何新类标记final时,都有一些好处。这告诉编译器它不需要查找任何更多的子类,它可以缩短编译时间;很明确的告诉你,不需要子类化的类标记了final。