2.13 继承
7 分钟阅读
原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/inheritance/
2.13 继承
用子类添加或覆盖功能。
一个类可以继承另一个类的方法、属性及其他特性。当一个类继承另一个类时,继承的类称为子类,被继承的类称为它的父类。继承是区分 Swift 中类与其他类型的一项基本行为。
Swift 中的类可以调用和访问其父类的方法、属性和下标,也可以为这些方法、属性和下标提供自己的覆盖版本,以改进或修改它们的行为。Swift 会检查覆盖定义与父类定义是否匹配,从而帮助确保覆盖正确无误。
类还可以为继承来的属性添加属性观察器,以便在属性值变化时得到通知。任何属性都可以添加属性观察器,无论它最初定义为存储属性还是计算属性。
定义基类
任何不继承其他类的类都称为基类。
注意:Swift 的类不继承自某个统一的基类。 你定义类时若不指定父类, 它就自动成为供你继续构建的基类。
下面的例子定义了一个名为 Vehicle 的基类。这个基类定义了一个名为 currentSpeed 的存储属性,默认值为 0.0(属性类型被推断为 Double)。currentSpeed 属性的值被一个名为 description 的只读计算 String 属性用来生成对车辆的描述。
Vehicle 基类还定义了一个名为 makeNoise 的方法。对于基类 Vehicle 的实例它实际上什么也不做,但稍后会被 Vehicle 的子类定制:
| |
你用构造器语法创建 Vehicle 的新实例,也就是类型名后跟一对空圆括号:
| |
创建了新的 Vehicle 实例之后,你可以访问它的 description 属性,打印出车辆当前速度的人类可读描述:
| |
Vehicle 类定义了任意车辆的共同特性,但它本身用处不大。要让它更有用,就需要对它加以细化,描述更具体的车辆种类。
子类化
子类化是在已有类的基础上建立新类的行为。子类继承已有类的特性,你可以对这些特性加以细化,也可以为子类添加新特性。
要表示某个子类有父类,就把子类名写在父类名之前,用冒号分隔:
| |
下面的例子定义了一个名为 Bicycle 的子类,它的父类是 Vehicle:
| |
新的 Bicycle 类自动获得 Vehicle 的所有特性,例如它的 currentSpeed、description 属性和 makeNoise() 方法。
除了继承来的特性,Bicycle 类还定义了一个新的存储属性 hasBasket,默认值为 false(推断出该属性的类型为 Bool)。
默认情况下,你创建的每个新 Bicycle 实例都没有车筐。你可以在某个 Bicycle 实例创建之后,把它的 hasBasket 属性设为 true:
| |
你也可以修改 Bicycle 实例继承来的 currentSpeed 属性,并查询该实例继承来的 description 属性:
| |
子类本身还可以再被继承。下一个例子为 Bicycle 创建了一个子类,表示被称为"双人车"的双座自行车:
| |
Tandem 继承了 Bicycle 的所有属性和方法,而 Bicycle 又继承了 Vehicle 的所有属性和方法。Tandem 子类还添加了一个名为 currentNumberOfPassengers 的新存储属性,默认值为 0。
如果你创建一个 Tandem 实例,就可以使用它新增的和继承来的任何属性,并查询它从 Vehicle 继承来的只读 description 属性:
| |
覆盖
子类可以为它原本会从父类继承的实例方法、类型方法、实例属性、类型属性或下标提供自己的自定义实现,这称为覆盖。
要覆盖原本会继承来的特性,就在覆盖定义前加上 override 关键字。这样做既表明你确实想提供一个覆盖,也表明你不是误写了一个同名的定义。意外覆盖可能导致意外行为,任何缺少 override 关键字的覆盖都会在编译代码时被诊断为错误。
override 关键字还会促使 Swift 编译器检查你正在覆盖的类的父类(或它的某个祖先)是否有与你提供的覆盖相匹配的声明,从而确保你的覆盖定义是正确的。
访问父类的方法、属性和下标
当你在子类中覆盖某个方法、属性或下标时,有时把父类已有的实现作为覆盖的一部分使用会很有用。例如,你可以改进该已有实现的行为,或者把修改后的值存进继承来的变量。
在这种情况下,你用 super 前缀访问父类版本的方法、属性或下标:
- 名为
someMethod()的被覆盖方法,可以在覆盖实现中调用super.someMethod()来调用父类版本的someMethod()。 - 名为
someProperty的被覆盖属性,可以在覆盖的取值器或设值器实现中用super.someProperty访问父类版本的someProperty。 - 针对
someIndex的被覆盖下标,可以在覆盖的下标实现中用super[someIndex]访问父类中同一个下标的版本。
覆盖方法
你可以覆盖继承来的实例方法或类型方法,在子类中为该方法提供量身定制或替代性的实现。
下面的例子为 Vehicle 定义了一个名为 Train 的新子类,它覆盖了 Train 从 Vehicle 继承来的 makeNoise() 方法:
| |
如果你创建一个新的 Train 实例并调用它的 makeNoise() 方法,就会看到调用的是 Train 子类版本的方法:
| |
覆盖属性
你可以覆盖继承来的实例属性或类型属性,为该属性提供自定义的取值器和设值器,或者添加属性观察器,使被覆盖的属性能够观察底层属性值的变化。
覆盖属性的取值器与设值器
你可以提供自定义的取值器(以及适当的设值器)来覆盖任何继承来的属性,无论该属性在源代码中实现为存储属性还是计算属性。子类并不知道继承来的属性是存储属性还是计算属性——它只知道这个继承来的属性有某个名字和类型。你必须始终同时写出被覆盖属性的名字和类型,以便编译器检查你的覆盖是否与同名同类型的父类属性匹配。
你可以在子类的属性覆盖中同时提供取值器和设值器,把继承来的只读属性呈现为读写属性;但你不能把继承来的读写属性呈现为只读属性。
注意:如果你在属性覆盖中提供了设值器, 就必须同时为该覆盖提供取值器。 如果你不想在覆盖的取值器中修改继承属性的值, 可以在取值器中返回
super.someProperty来直接沿用继承来的值, 其中someProperty是你要覆盖的属性名。
下面的例子定义了一个新类 Car,它是 Vehicle 的子类。Car 类引入了一个名为 gear 的新存储属性,默认整数值为 1。Car 类还覆盖了它从 Vehicle 继承来的 description 属性,提供包含当前挡位的自定义描述:
| |
对 description 属性的覆盖首先调用 super.description,返回 Vehicle 类的 description 属性;随后 Car 类版本的 description 在这段描述末尾追加一些文字,提供当前挡位的信息。
如果你创建一个 Car 类实例并设置它的 gear 和 currentSpeed 属性,就能看到它的 description 属性返回的是 Car 类中定义的定制描述:
| |
覆盖属性观察器
你可以用属性覆盖为继承来的属性添加属性观察器,从而在该继承属性的值变化时得到通知,无论这个属性最初是如何实现的。关于属性观察器的更多内容,参见属性观察器。
注意:你不能为继承来的常量存储属性 或继承来的只读计算属性添加属性观察器。 这些属性的值无法设置, 因此作为覆盖的一部分提供
willSet或didSet实现并不合适。另请注意,你不能为同一个属性同时提供 覆盖设值器和覆盖属性观察器。 如果你想观察属性值的变化, 而已经为该属性提供了自定义设值器, 那么直接在自定义设值器中观察值的变化即可。
下面的例子定义了一个新类 AutomaticCar,它是 Car 的子类。AutomaticCar 类表示一辆带自动变速箱的汽车,它会根据当前车速自动选择合适的挡位:
| |
每当你设置 AutomaticCar 实例的 currentSpeed 属性时,该属性的 didSet 观察器就把实例的 gear 属性设为适合新车速的挡位。具体来说,观察器选择的挡位是新 currentSpeed 值除以 10 之后向下取整再加 1。速度 35.0 对应的挡位是 4:
| |
阻止覆盖
把方法、属性或下标标记为 final,就可以防止它被覆盖。为此,把 final 修饰符写在方法、属性或下标的引入关键字之前(例如 final var、final func、final class func 和 final subscript)。
任何在子类中覆盖 final 方法、属性或下标的尝试都会被报告为编译期错误。你在扩展中为类添加的方法、属性或下标,也可以在扩展的定义中标记为 final。更多内容参见扩展。
在类的定义中把 final 修饰符写在 class 关键字之前(即 final class),就可以把整个类标记为 final。任何继承 final 类的尝试都会被报告为编译期错误。