类的继承和构造过程
类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。
指定构造器和便利构造器
指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。
每一个类都必须拥有至少一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。
便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。
你应当只在必要的时候为类提供便利构造器,比方说某种情况下通过使用便利构造器来快捷调用某个指定构造器,能够节省更多开发时间并让类的构造过程更清晰明了。
指定构造器和便利构造器的语法
类的指定构造器的写法跟值类型简单构造器一样:
init(parameters) {
statements
}
便利构造器也采用相同样式的写法,但需要在init关键字之前放置convenience关键字,并使用空格将它们俩分开:
convenience init(parameters) {
statements
}
类的构造器代理规则
为了简化指定构造器和便利构造器之间的调用关系,Swift 采用以下三条规则来限制构造器之间的代理调用:
为了简化指定构造器和便利构造器之间的调用关系,Swift 采用以下三条规则来限制构造器之间的代理调用:
-
规则 1
指定构造器必须调用其直接父类的的指定构造器。 -
规则 2
便利构造器必须调用同类中定义的其它构造器。 -
规则 3
便利构造器必须最终导致一个指定构造器被调用。
一个更方便记忆的方法是:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
两段式构造过程
Swift 中类的构造过程包含两个阶段。第一个阶段,每个存储型属性被引入它们的类指定一个初始值。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。
两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。
注意
Swift 的两段式构造过程跟 Objective-C 中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值0或空值(比如说0或nil)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以0或nil作为合法默认值的情况。
Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程能不出错地完成:
安全检查 1
指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类引入的属性在它往上代理之前先完成初始化。
安全检查 2
指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
安全检查 3
便利构造器必须先代理调用同一类中的其它构造器,然后再为任意属性赋新值。如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。
安全检查 4
构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值
类实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,该实例才会成为有效实例,才能访问属性和调用方法。
阶段 1
- 某个指定构造器或便利构造器被调用。
- 完成新实例内存的分配,但此时内存还没有被初始化。
- 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
- 指定构造器将调用父类的构造器,完成父类属性的初始化。
- 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。
- 当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2
- 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。
- 最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。
构造器的继承和重写
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,并被错误地用来创建子类的实例。
注意
父类的构造器仅会在安全和适当的情况下被继承。具体内容请参考后续章节构造器的自动继承
假如你希望自定义的子类中能提供一个或多个跟父类相同的构造器,你可以在子类中提供这些构造器的自定义实现。
当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上override
修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上override
修饰符,具体内容请参考默认构造器。
正如重写属性,方法或者是下标,override修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否正确。
注意
当你重写一个父类的指定构造器时,你总是需要写override修饰符,即使你的子类将父类的指定构造器重写为了便利构造器。
相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文类的构造器代理规则有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加override
前缀。
在下面的例子中定义了一个叫Vehicle
的基类。基类中声明了一个存储型属性numberOfWheels
,它是值为0
的Int
类型的存储型属性。numberOfWheels
属性用于创建名为descrpiption
的String
类型的计算型属性:
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
Vehicle
类只为存储型属性提供默认值,而不自定义构造器。因此,它会自动获得一个默认构造器,具体内容请参考默认构造器。自动获得的默认构造器总会是类中的指定构造器,它可以用于创建numberOfWheels
为0
的Vehicle
实例:
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 个规则适用:
规则 1
如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承所有父类的便利构造器。
即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。
注意
对于规则 2,子类可以将父类的指定构造器实现为便利构造器。
指定构造器和便利构造器实践
接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类Food、RecipeIngredient以及ShoppingListItem的类层次结构,并将演示它们的构造器是如何相互作用的。
类层次中的基类是Food,它是一个简单的用来封装食物名字的类。Food类引入了一个叫做name的String类型的属性,并且提供了两个构造器来创建Food实例:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
类类型没有默认的逐一成员构造器,所以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的子类RecipeIngredient。RecipeIngredient类用来表示食谱中的一项原料。它引入了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
类拥有一个指定构造器init(name: String, quantity: Int)
,它可以用来填充RecipeIngredient
实例的所有属性值。这个构造器一开始先将传入的quantity
参数赋值给quantity
属性,这个属性也是唯一在RecipeIngredient
中新引入的属性。随后,构造器向上代理到父类Food
的init(name: String)
。这个过程满足两段式构造过程中的安全检查 1。
RecipeIngredient还定义了一个便利构造器init(name: String),它只通过name来创建RecipeIngredient的实例。这个便利构造器假设任意RecipeIngredient实例的quantity为1,所以不需要显式指明数量即可创建出实例。这个便利构造器的定义可以更加方便和快捷地创建实例,并且避免了创建多个quantity为1的RecipeIngredient实例时的代码重复。这个便利构造器只是简单地横向代理到类中的指定构造器,并为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引入了一个布尔类型的属性purchased,它的默认值是false。ShoppingListItem还添加了一个计算型属性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,并标记为已购买。打印数组中每个元素的描述显示了它们都已按照预期被赋值。
网友评论