15.5. 类的继承和构造过程

里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。

Swift类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们被称为指定构造器和便利构造器。

指定构造器和便利构造器

指定构造器中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。

倾向于拥有极少的指定构造器,普遍的是一个类只拥有一个指定构造器。指定构造器像一个个“漏斗”放在构造过程发生的地方,让构造过程沿着父类链继续往上进行。

每一个都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。具体内容请参考后续章节构造器的自动继承。

便利构造器中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

你应当只在必要的时候为提供便利构造器,比方说某种情况下通过使用便利构造器来快捷调用某个指定构造器,能够节省更多开发时间并让类的构造过程更清晰明了。

指定构造器和便利构造器的语法

的指定构造器的写法跟值类型简单构造器一样:

init(parameters) {
    statements
}

便利构造器也采用相同样式的写法,但需要在 init 关键字之前放置 convenience 关键字,并使用空格将它们俩分开:

convenience init(parameters) {
    statements
}

类类型的构造器代理

为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用遵循以下三条规则:

  • 规则 1:指定构造器必须调用其直接父类的的指定构造器。
  • 规则 2:便利构造器必须调用类中定义的其它构造器。
  • 规则 3:便利构造器最后必须调用指定构造器。

一个更方便记忆的方法是:

  • 指定构造器必须总是向上代理
  • 便利构造器必须总是横向代理

这些规则可以通过下面图例来说明:

构造器代理图构造器代理图

如图所示,父中包含一个指定构造器和两个便利构造器。其中一个便利构造器调用了另外一个便利构造器,而后者又调用了唯一的指定构造器。这满足了上面提到的规则 2 和 3。这个父类没有自己的父类,所以规则 1 没有用到。

中包含两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的任意一个,因为它只能调用同一个类里的其他构造器。这满足了上面提到的规则 2 和 3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则 1。

注意

这些规则不会影响类的实例如何创建。任何上图中展示的构造器都可以用来创建完全初始化的实例。这些规则只影响类的构造器如何实现。

下面图例中展示了一种涉及四个的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“漏斗”的作用,在类的构造器链上简化了类之间的相互关系。

复杂构造器代理图复杂构造器代理图

两段式构造过程

Swift的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。

两段式构造过程的使用让构造过程更安全,同时在整个层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

注意

Swift 的两段式构造过程跟 Objective-C 中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值 0 或空值(比如说 0nil)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以 0nil 作为合法默认值的情况。

Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成:

  • 安全检查 1:指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。
  • 安全检查 2:指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
  • 安全检查 3:便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做,便利构造器赋予的新值将被该类的指定构造器所覆盖。
  • 安全检查 4:构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

的实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,类的实例才是有效的,才能访问属性和调用方法。

以下是基于上述安全检查的两段式构造过程展示:

  • 阶段 1:
    • 类的某个指定构造器或便利构造器被调用。
    • 完成类的新实例内存的分配,但此时内存还没有被初始化。
    • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
    • 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
    • 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
    • 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
  • 阶段 2:
    • 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
    • 最终,继承链中任意的便利构造器有机会自定义实例和使用 self

下图展示了在假定的子和父类之间的构造阶段 1:

构建过程阶段1构建过程阶段1

在这个例子中,构造过程从对子中一个便利构造器的调用开始。这个便利构造器此时还不能修改任何属性,它会代理到该类中的指定构造器。

如安全检查 1 所示,指定构造器将确保所有子的属性都有值。然后它将调用父类的指定构造器,并沿着继承链一直往上完成父类的构造过程。

中的指定构造器确保所有父类的属性都有值。由于没有更多的父类需要初始化,也就无需继续向上代理。

一旦父中所有属性都有了初始值,实例的内存被认为是完全初始化,阶段 1 完成。

以下展示了相同构造过程的阶段 2:

构建过程阶段2构建过程阶段2

中的指定构造器现在有机会进一步自定义实例(尽管这不是必须的)。

一旦父中的指定构造器完成调用,子类中的指定构造器可以执行更多的自定义操作(这也不是必须的)。

最终,一旦子的指定构造器完成调用,最开始被调用的便利构造器可以执行更多的自定义操作。

构造器的继承和重写

跟 Objective-C 中的子不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。

注意

父类的构造器仅会在安全和适当的某些情况下被继承。具体内容请参考后续章节构造器的自动继承。

假如你希望自定义的子中能提供一个或多个跟父类相同的构造器,你可以在子类中提供这些构造器的自定义实现。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上 override 修饰符,具体内容请参考 15.3. 默认构造器

正如重写属性,方法或者是下标,override 修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否被按预想中被指定。

注意

当你重写一个父类的指定构造器时,你总是需要写 override 修饰符,即使是为了实现子类的便利构造器。

相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文类的构造器代理规则有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加 override 修饰符。

在下面的例子中定义了一个叫 Vehicle 的基类。基类中声明了一个存储型属性 numberOfWheels,它是默认值为 Int 类型的 0numberOfWheels 属性用在一个描述车辆特征 String 类型为 descrpiption 的计算型属性中:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Vehicle 类只为存储型属性提供默认值,也没有提供自定义构造器。因此,它会自动获得一个默认构造器,具体内容请参考 15.3. 默认构造器。默认构造器(如果有的话)总是类中的指定构造器,可以用于创建 numberOfWheels0Vehicle 实例:

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

下面例子中定义了一个 Vehicle 的子类 Bicycle

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

子类 Bicycle 定义了一个自定义指定构造器 init()。这个指定构造器和父类的指定构造器相匹配,所以 Bicycle 中这个版本的构造器需要带上 override 修饰符。

Bicycle 的构造器 init() 以调用 super.init() 方法开始,这个方法的作用是调用 Bicycle 的父类 Vehicle 的默认构造器。这样可以确保 Bicycle 在修改属性之前,它所继承的属性 numberOfWheels 能被 Vehicle 类初始化。在调用 super.init() 之后,属性 numberOfWheels 的原值被新值 2 替换。

如果你创建一个 Bicycle 实例,你可以调用继承的 description 计算型属性去查看属性 numberOfWheels 是否有改变:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// 打印“Bicycle: 2 wheel(s)”

如果子类的构造器没有在阶段 2 过程中做自定义操作,并且父类有一个无参数的指定构造器,你可以在所有子类的存储属性赋值之后省略 super.init() 的调用。

这个例子定义了另一个 Vehicle 的子类 Hoverboard ,只设置它的 color 属性。这个构造器依赖隐式调用父类的构造器来完成,而不是显示调用 super.init()

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() 在这里被隐式调用
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

Hoverboard 的实例用 Vehicle 构造器里默认的轮子数量。

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

注意

子类可以在构造过程修改继承来的变量属性,但是不能修改继承来的常量属性。

构造器的自动继承

如上所述,子在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假设你为子中引入的所有新属性都提供了默认值,以下 2 个规则将适用:

  • 规则 1:如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
  • 规则 2:如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。

即使你在子中添加了更多的便利构造器,这两条规则仍然适用。

注意

子类可以将父类的指定构造器实现为便利构造器来满足规则 2。

指定构造器和便利构造器实践

接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类 FoodRecipeIngredient 以及 ShoppingListItem 的层级结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是 Food,它是一个简单的用来封装食物名字的类。Food 类引入了一个叫做 nameString 类型的属性,并且提供了两个构造器来创建 Food 实例:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

下图中展示了 Food 的构造器链:

Food 构造器链Food 构造器链

类类型没有默认的逐一成员构造器,所以 Food 类提供了一个接受单一参数 name 的指定构造器。这个构造器可以使用一个特定的名字来创建新的 Food 实例:

let namedMeat = Food(name: "Bacon")
// namedMeat 的名字是 "Bacon"

Food 类中的构造器 init(name: String) 被定义为一个指定构造器,因为它能确保 Food 实例的所有存储型属性都被初始化。Food 类没有父类,所以 init(name: String) 构造器不需要调用 super.init() 来完成构造过程。

Food 类同样提供了一个没有参数的便利构造器 init()。这个 init() 构造器为新食物提供了一个默认的占位名字,通过横向代理到指定构造器 init(name: String) 并给参数 name 赋值为 [Unnamed] 来实现:

let mysteryMeat = Food()
// mysteryMeat 的名字是 [Unnamed]

层级中的第二个类是 Food 的子类 RecipeIngredientRecipeIngredient 类用来表示食谱中的一项原料。它引入了 Int 类型的属性 quantity(以及从 Food 继承过来的 name 属性),并且定义了两个构造器来创建 RecipeIngredient 实例:

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

下图中展示了 RecipeIngredient 类的构造器链:

RecipeIngredient 构造器RecipeIngredient 构造器

RecipeIngredient 类拥有一个指定构造器 init(name: String, quantity: Int),它可以用来填充 RecipeIngredient 实例的所有属性值。这个构造器一开始先将传入的 quantity 实参赋值给 quantity 属性,这个属性也是唯一在 RecipeIngredient 中新引入的属性。随后,构造器向上代理到父类 Foodinit(name: String)。这个过程满足两段式构造过程中的安全检查 1。

RecipeIngredient 也定义了一个便利构造器 init(name: String),它只通过 name 来创建 RecipeIngredient 的实例。这个便利构造器假设任意 RecipeIngredient 实例的 quantity1,所以不需要显式的质量即可创建出实例。这个便利构造器的定义可以更加方便和快捷地创建实例,并且避免了创建多个 quantity1RecipeIngredient 实例时的代码重复。这个便利构造器只是简单地横向代理到类中的指定构造器,并为 quantity 参数传递 1

RecipeIngredient 的便利构造器 init(name: String) 使用了跟 Food 中指定构造器 init(name: String) 相同的形参。由于这个便利构造器重写了父类的指定构造器 init(name: String),因此必须在前面使用 override 修饰符(参见上面的构造器的继承和重写)。

尽管 RecipeIngredient 将父类的指定构造器重写为了便利构造器,但是它依然提供了父类的所有指定构造器的实现。因此,RecipeIngredient 会自动继承父类的所有便利构造器。

在这个例子中,RecipeIngredient 的父类是 Food,它有一个便利构造器 init()。这个便利构造器会被 RecipeIngredient 继承。这个继承版本的 init() 在功能上跟 Food 提供的版本是一样的,只是它会代理到 RecipeIngredient 版本的 init(name: String) 而不是 Food 提供的版本。

所有的这三种构造器都可以用来创建新的 RecipeIngredient 实例:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

类层级中第三个也是最后一个类是 RecipeIngredient 的子类,叫做 ShoppingListItem。这个类构建了购物单中出现的某一种食谱原料。

购物单中的每一项总是从未购买状态开始的。为了呈现这一事实,ShoppingListItem 引入了一个 Boolean(布尔类型) 的属性 purchased,它的默认值是 falseShoppingListItem 还添加了一个计算型属性 description,它提供了关于 ShoppingListItem 实例的一些文字描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

注意

ShoppingListItem 没有定义构造器来为 purchased 提供初始值,因为添加到购物单的物品的初始状态总是未购买。

因为它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem 将自动继承所有父类中的指定构造器和便利构造器。

下图展示了这三个的构造器链:

三类构造器图三类构造器图

你可以使用三个继承来的构造器来创建 ShoppingListItem 的新实例:

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x orange juice ✔
// 1 x bacon ✘
// 6 x eggs ✘

如上所述,例子中通过字面量方式创建了一个数组 breakfastList,它包含了三个 ShoppingListItem 实例,因此数组的类型也能被自动推导为 [ShoppingListItem]。在数组创建完之后,数组中第一个 ShoppingListItem 实例的名字从 [Unnamed] 更改为 Orange juice,并标记状态为已购买。打印数组中每个元素的描述显示了它们都已按照预期被赋值。

下一节:Swift 是一种非常好的编写软件的方式,无论是手机,台式机,服务器,还是其他运行代码的设备。它是一种安全,快速和互动的编程语言,将现代编程语言的精华和苹果工程师文化的智慧,以及来自开源社区的多样化贡献结合了起来。编译器对性能进行了优化,编程语言对开发进行了优化,两者互不干扰,鱼与熊掌兼得。