现在位置: 首页 > Swift 教程 > 正文

Swift 继承

继承是类区别于结构体和枚举的核心能力:一个类可以获取另一个类的属性、方法和下标。

被继承的类叫父类(也叫超类、基类),继承的类叫子类。

子类除了直接使用父类的成员,还能重写它们,或者加上自己的新成员。

Swift 中只有类支持继承,结构体和枚举都不行,这一点在《Swift 结构体》里已经对比过。


基类与子类

没有被任何类继承的类叫基类,Swift 里不需要像 Java 那样显式写一个根父类。

下面的 RunoobEmployee 就是一个基类,它有 name、salary 两个存储属性和一个 describe() 方法。

实例

import Foundation

// 基类:没有继承任何其他类的类
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 关键字重写它。

实例

import Foundation

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。

实例

import Foundation

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)属性」,不是「重载属性」。属性没有参数列表,不存在重载的说法。

重写计算属性

父类的计算属性可以在子类里给出另一套计算逻辑。

实例

import Foundation

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 后面没有括号。

实例

import Foundation

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。

下面的例子重写了父类的下标,把越界访问从崩溃改成返回占位符。

实例

import Foundation

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 标记。

实例

import Foundation

// 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 例子已经演示过。