高级类(三)
何时以及为什么要子类化。
本章介绍了类的继承,以及子类化相关的众多编程技术。
但是你可能会问,“什么时候我应该子类化?”
对于这个问题,很少有正确或错误的答案。辩证的看点这个问题可以帮助你为任何特殊情况做出最佳决策。
以Student学生和StudentAthlete学生运动员类为例,你可以简单地把StudentAthlete学生运动员的所有特征都放在Student学生身上:
class Student: Person {
var grades: [Grade]
var sports: [Sport]
// original code
}
这可以解决所有需要的用例。一个不做运动的学生只会有一个空的sports数组,你可以避免一些额外的复杂的子类化。
单一职责
在软件开发中,单一职责是说,声明的任何类都应该有一个单独职责。在学生和学生运动员中,学生有学生的责任,运动员有运动员的责任,学生的责任不应该封装到运动员里,运动员的也不应该封装到学生里。
强类型
使用Swift的类型系统,子类化创建了一个附加类型。你可以基于学生运动员而不是普通学生的对象声明属性或行为:
class Team {
var players: [StudentAthlete] = []
var isEligible: Bool {
for player in players {
if !player.isEligible {
return false
}
}
return true
}
}
一个队有运动员,他们是学生运动员。如果你试图将一个普通的学生对象添加到players数组中,类型系统将不允许。这很有用,因为编译器可以帮助你执行系统的逻辑和需求。
共享基类
你可以通过具有互斥行为的类,多次子类化一个共享基类:
// 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
}
}
在本例中,你可以想象许多Button子类只共享按下的事件。因此当按下按钮时,Button子类必须实现自己的行为。ImageButton和TextButton类有完全不同的机制来呈现按钮的外观。
你可以在这里看到,在Button类中存储图像和文本——更不用说可能出现的任何其他类型的按钮,还会有其他的样式。因此按钮与媒体行为有关,只有处理按钮的实际外观的子类才是有意义的。
可扩展性
有时,如果你要扩展你的代码不拥有的行为,那么你就必须进行子类化。在上面的示例中,可能Button是你正在使用的框架的一部分,并且你不可能修改或扩展源代码以满足你的需要。
在这种情况下,子类化Button可以添加自定义子类,并使用这种类型按钮对象来使用Button。
同一性
最后,重要的是要理解类和类层次结构模型的对象是什么。如果你的目标是在类型之间共享行为(对象可以做什么),那么通常你应该更喜欢协议而不是子类化。
理解类的生命周期
在前一章中,你了解到对象是在内存中创建的,它们存储在堆中。堆上的对象不会被自动销毁,因为堆只是一个巨大的内存池。如果没有调用堆栈的实用程序,就没有自动的方法来知道一个内存将不再被使用。
在Swift中,决定何时清理堆上未使用的对象的机制称为引用计数。简而言之,每个对象都有一个引用计数,每一个常量或变量对该对象的引用都会递增,并且每次删除引用时都会递减。
注意:在其他书籍和在线资源中,你可能会看到引用计数被称为“保留计数”。他们指的是同一件事!
当引用计数达到0时,这意味着该对象现在被放弃,因为系统中没有任何引用。当这种情况发生时,Swift将清理对象。
这里展示了一个对象的引用计数的变化。注意,在这个示例中只创建了一个实际对象;一个对象有很多引用。
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
在本例中,你不需要自己做任何工作来增加或减少对象的引用计数。这是因为Swift具有自动引用计数或ARC的特性。
虽然一些较旧的语言要求你在代码中增加和减少引用计数,但是Swift编译器在编译时自动添加这些调用。
注意:如果你使用像C这样的低级语言,那么你需要手动释放内存。像Java和c#这样的高级语言使用了称为垃圾收集的东西。在这种情况下,运行时,将在清理不再使用的对象之前,搜索进程引用的对象。垃圾收集虽然比ARC更强大,但它带来的内存利用率低和性能成本高,所以苹果公司决定不接受移动设备或通用系统语言。
Deinitialization
当一个对象的引用计数达到0时,Swift将对象从内存中移除,并将其标记为空闲内存。
deinitializer是一个特殊的方法,它在对象的引用计数达到0时运行,但是在Swift将对象从内存中移除之前。
修改类Person如下:
class Person {
// original code
deinit {
print("\(firstName) \(lastName) is being removed
from memory!")
}
}
很像init是类初始化中的一种特殊方法,deinit是一个处理初始化的特殊方法。与init不同,deinit不是必需的,并且会被Swift自动调用。你也不需要覆盖它或在其中调用super。Swift将确保调用每个类的deinitializer。
如果你添加这个deinitializer,在运行前一个示例后的调试区域中,你将看到Johnny Appleseed正在被从内存中删除的消息!
你在deinitializer中所做的事情取决于你。通常,你将使用它来清理其他资源,将状态保存到磁盘,或者在对象超出范围时执行你可能需要的任何其他逻辑。
循环引用和弱引用
由于Swift的类依赖于引用计数来将它们从内存中删除,所以理解retain cycle的概念非常重要。
添加一个代表同学的字段—例如,一个实验室伙伴—和一个deinitializer方法,这样的学生:
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和bob都辍学了:
alice = nil
bob = nil
如果你在你的playground上运行这个,你会注意到,Swift不调用deinit。这是为什么呢?
Alice和Bob各自有一个引用,所以引用计数永远不会达到零!更糟糕的是,通过给alice和bob分配nil,就没有对初始对象的引用了。这是一个循环引用的经典案例,它导致了一个被称为内存泄漏的软件缺陷。
在内存泄漏的情况下,即使它的实际生命周期已经结束,内存也不会释放出来。循环引用是内存泄漏最常见的原因。
幸运的是,有一种方法,学生对象可以引用另一个学生而不容易循环引用,这是通过使用弱引用。
class Student: Person {
weak var partner: Student?
// original code
}
这个简单的修改将伙伴变量标记为弱,这意味着该变量中的引用不会参与引用计数。当引用不弱时,它被称为强引用,这是Swift的默认值。弱引用必须声明为可选类型,以便当它们引用的对象被释放时,它会自动变为nil。
关键点
•类继承是类最重要的特性之一,它支持多态性。
•Swift类使用两阶段初始化作为安全措施,确保在使用所有存储属性之前都进行了初始化。
•子类化是一个强大的工具,但是知道什么时候子类化是很好的。当你想要扩展一个对象,并且可以从子类和超类之间的“is-a”关系中受益,但是要注意继承的状态和深层的类层次结构。
•类实例有它们自己的生命周期,它们由它们的引用计数控制。
•自动引用计数,或ARC,称为自动处理引用计数,但重要的是要注意循环引用。