指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。
类倾向于拥有极少的指定构造器,普遍的是一个类只拥有一个指定构造器。指定构造器像一个个“漏斗”放在构造过程发生的地方,让构造过程沿着父类链继续往上进行。
每一个类都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。具体内容请参考后续章节 构造器的自动继承。
便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。
你应当只在必要的时候为类提供便利构造器,比方说某种情况下通过使用便利构造器来快捷调用某个指定构造器,能够节省更多开发时间并让类的构造过程更清晰明了。
指定构造器和便利构造器的语法
类的指定构造器的写法跟值类型简单构造器一样:
init(parameters) { statements }
便利构造器也采用相同样式的写法,但需要在 init
关键字之前放置 convenience
关键字,并使用空格将它们俩分开:
convenience init(parameters) { statements }
类类型的构造器代理
为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用遵循以下三条规则:
规则 1
指定构造器必须调用其直接父类的的指定构造器。
规则 2
便利构造器必须调用同类中定义的其它构造器。
规则 3
便利构造器最后必须调用指定构造器。
一个更方便记忆的方法是:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
这些规则可以通过下面图例来说明:
如图所示,父类中包含一个指定构造器和两个便利构造器。其中一个便利构造器调用了另外一个便利构造器,而后者又调用了唯一的指定构造器。这满足了上面提到的规则 2 和 3。这个父类没有自己的父类,所以规则 1 没有用到。
子类中包含两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的任意一个,因为它只能调用同一个类里的其他构造器。这满足了上面提到的规则 2 和 3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则 1。
注意
这些规则不会影响类的实例如何创建。任何上图中展示的构造器都可以用来创建完全初始化的实例。这些规则只影响类的构造器如何实现。
下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“漏斗”的作用,在类的构造器链上简化了类之间的相互关系。
接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类 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
的构造器链:
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
的子类 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
类的构造器链:
RecipeIngredient 构造器
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
引入了一个 Boolean(布尔类型) 的属性 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
,并标记状态为已购买。打印数组中每个元素的描述显示了它们都已按照预期被赋值。