Swift 继承
继承是类区别于结构体和枚举的核心能力:一个类可以获取另一个类的属性、方法和下标。
被继承的类叫父类(也叫超类、基类),继承的类叫子类。
子类除了直接使用父类的成员,还能重写它们,或者加上自己的新成员。
Swift 中只有类支持继承,结构体和枚举都不行,这一点在《Swift 结构体》里已经对比过。
基类与子类
没有被任何类继承的类叫基类,Swift 里不需要像 Java 那样显式写一个根父类。
下面的 RunoobEmployee 就是一个基类,它有 name、salary 两个存储属性和一个 describe() 方法。
实例
// 基类:没有继承任何其他类的类
class RunoobEmployee {
var name: String
var salary: Double
init(name: String, salary: Double) {
self.name = name
self.salary = salary
}
func describe() -> String {
return "\(name) 的月薪是 \(salary)"
}
}
// 子类:继承 RunoobEmployee,并新增自己的存储属性
class RunoobManager: RunoobEmployee {
var bonus: Double
init(name: String, salary: Double, bonus: Double) {
self.bonus = bonus // 先初始化子类自己的存储属性
super.init(name: name, salary: salary) // 再调用父类构造器
}
func totalIncome() -> Double {
return salary * 12 + bonus
}
}
let employee = RunoobEmployee(name: "小明", salary: 8000)
print(employee.describe())
let manager = RunoobManager(name: "小红", salary: 15000, bonus: 30000)
print(manager.describe()) // 调用继承自父类的方法
print("年收入:\(manager.totalIncome())")
运行结果:
小明 的月薪是 8000.0 小红 的月薪是 15000.0 年收入:210000.0
子类语法的关键是在类名后写冒号和父类名,多个协议或父类之间用逗号分隔。
子类实例可以直接读父类的 salary,说明继承来的成员对子类是可见的。
构造顺序不能颠倒
子类构造器里必须先给自己新增的存储属性赋值,再调用 super.init。
反过来先调 super.init,编译器会报错:子类的属性还没有初始值,Swift 不允许对象处于半初始化状态。
这套规则属于两段式构造过程,细节见《Swift 构造过程》。
重写方法
如果子类对父类方法的实现不满意,可以用 override 关键字重写它。
实例
class RunoobStaff {
var name: String
init(name: String) { self.name = name }
func duty() -> String {
return "\(name):完成日常开发任务"
}
}
class RunoobLeader: RunoobStaff {
// override 表明这是对父类方法的重写
override func duty() -> String {
let base = super.duty() // 先调用父类的实现
return base + ",并负责团队管理"
}
}
print(RunoobLeader(name: "小红").duty())
运行结果:
小红:完成日常开发任务,并负责团队管理
重写方法的签名必须和父类完全一致,包括方法名、参数类型和返回类型。
注意:忘了写 override 会编译报错;反过来,父类根本没有这个方法却写了 override,同样会报错。这两个错误都是编译器帮你兜底,不要靠运行期去发现。
重写属性
属性也能重写,常见有两种做法:给继承来的属性加观察器,或者重写计算属性。
为继承来的属性添加属性观察器
不管父类里这个属性是存储属性还是计算属性,子类都可以通过重写的方式给它挂上 willSet / didSet。
实例
class RunoobAccount {
var balance: Double = 0
}
class RunoobSavingsAccount: RunoobAccount {
// 重写父类的存储属性,为它添加属性观察器
override var balance: Double {
didSet {
// 每次赋值后触发,oldValue 是本次写入前的值
print("余额已更新为 \(balance),本次变动 \(balance - oldValue)")
}
}
}
let account = RunoobSavingsAccount()
account.balance = 100
account.balance = 250
运行结果:
余额已更新为 100.0,本次变动 100.0 余额已更新为 250.0,本次变动 150.0
注意两次输出都出现在赋值之后,构造器里给 balance 赋初值 0 时并没有触发观察器。
注意:这是「重写(override)属性」,不是「重载属性」。属性没有参数列表,不存在重载的说法。
重写计算属性
父类的计算属性可以在子类里给出另一套计算逻辑。
实例
class RunoobShape {
var name: String { "形状" }
var detail: String { "这是一个\(name)" }
}
class RunoobCircle: RunoobShape {
// 重写只读计算属性,只提供 get 即可
override var name: String { "圆形" }
}
print(RunoobCircle().detail)
运行结果:
这是一个圆形
父类 detail 里调用的是 name,而 name 被子类重写,所以最终拼出的是「这是一个圆形」。
这说明属性的访问也是动态派发的,只要子类提供了新实现,父类代码中对该属性的读取同样会走到子类版本。
重写只读计算属性时只写 get;只读可以被重写成可读写,反过来把可读写重写成只读则不被允许。
另外,用 let 声明的存储属性不能被重写,常量在初始化后就固定了。
super 访问
重写不是「推倒重来」,很多时候子类想复用父类的实现再补充,这时用 super。
super 的写法和 self 类似,但访问的成员类型不同,写法也有区别。
| 访问对象 | 写法 | 说明 |
|---|---|---|
| 父类方法 | super.duty() | 方法调用带括号 |
| 父类属性 | super.doubled | 属性不带括号 |
| 父类下标 | super[index] | 用方括号访问 |
| 父类构造器 | super.init(...) | 只能在子类构造器中调用 |
下面的例子重点看属性的写法:super.doubled 后面没有括号。
实例
class RunoobCounter {
var count: Int = 0
var doubled: Int { count * 2 }
}
class RunoobLoggedCounter: RunoobCounter {
override var doubled: Int {
// super.doubled 访问父类属性,属性不带括号
let parentValue = super.doubled
print("父类计算结果是 \(parentValue)")
return parentValue + 1
}
}
let counter = RunoobLoggedCounter()
counter.count = 5
print(counter.doubled)
运行结果:
父类计算结果是 10 11
注意:super.doubled 是属性访问,写成 super.doubled() 会编译报错。方法才需要括号,属性和下标不需要。
重写下标
下标本质上是一段带参数的访问代码,同样可以被重写,语法是在 subscript 前加 override。
下面的例子重写了父类的下标,把越界访问从崩溃改成返回占位符。
实例
class RunoobList {
var items: [String] = ["a", "b", "c"]
subscript(index: Int) -> String {
get { items[index] }
set { items[index] = newValue }
}
}
class RunoobSafeList: RunoobList {
override subscript(index: Int) -> String {
get {
// 越界时返回占位符,而不是让程序崩溃
if index < 0 || index >= items.count { return "越界" }
return super[index]
}
set {
if index >= 0 && index < items.count {
super[index] = newValue
}
}
}
}
let list = RunoobSafeList()
print(list[1])
print(list[10])
list[0] = "runoob"
print(list[0])
运行结果:
b 越界 runoob
重写下标时,参数个数、参数类型和返回类型必须与父类一致。
只读下标可以被重写成可读写下标,但可读写下标不能被重写成只读下标。
关于下标的完整语法,可以参考《Swift 下标》。
防止重写:final
如果某个方法、属性或下标不希望被子类改动,用 final 标记。
实例
// final 加在类上,表示整个类不允许被继承
final class RunoobUtility {
func run() { print("工具类的方法") }
}
class RunoobBase {
final func run() { print("这个方法不允许被重写") } // 单个方法禁止重写
func config() { print("这个方法可以重写") }
}
class RunoobDerived: RunoobBase {
override func config() { print("子类重写的 config") }
}
RunoobUtility().run()
RunoobDerived().run()
RunoobDerived().config()
运行结果:
工具类的方法 这个方法不允许被重写 子类重写的 config
如果硬要重写 final 方法,编译器会直接拦下来:
error: instance method overrides a 'final' instance method
同样地,继承一个 final 类也会报错。
final 除了表达设计意图,还有一个附带好处:编译器知道这个方法不会被重写,就能改用静态派发,省掉查表开销。
注意:final 的「不允许跨模块继承」和访问控制里的 open 是两回事,open 管的是可见性与可继承性的组合,见《Swift 访问控制》。
常见问题
下面整理了继承相关的几个高频疑问。
子类能继承父类的构造器吗?
有条件地继承。如果子类没有定义任何指定构造器,它会自动继承父类的全部指定构造器。
如果子类实现了父类全部指定构造器(不管是自己写的还是继承来的),它还会额外继承父类的便利构造器。
一旦子类定义了自己的指定构造器,就没有这套自动继承了,需要自己安排构造器。
父类的 private 成员子类能访问吗?
不能。private 的作用域是声明所在的词法作用域,再加上同文件的扩展。
子类即使和父类写在同一个文件里,也不属于这个作用域,需要更宽的 fileprivate 或 internal 才行。
结构体可以继承吗?
不可以。结构体是值类型,不支持继承,也没有父类的概念。
需要共享实现时,结构体通常改用协议和协议扩展,见《Swift 协议扩展与面向协议编程》。
重写属性和重写方法在派发上一样吗?
对类而言一样,都是动态派发,调用时按实例的真实类型决定用哪个实现。
这也是父类代码里读到的属性可能是子类版本的原因,前面的 RunoobShape 例子已经演示过。
