Swift 的类、结构体、枚举等的构造过程Initialization(下)

时间:2021-01-11 16:39:43

类的继承和构造过程

类里面的全部存储型属性--包含全部继承自父类的属性--都必须在构造过程中设置初始值。

Swift 提供了两种类型的类构造器来确保全部类实例中存储型属性都能获得初始值,它们各自是指定构造器和便利构造器。

指定构造器和便利构造器

指定构造器是类中最基本的构造器。一个指定构造器将初始化类中提供的全部属性,并依据父类链往上调用父类的构造器来实现父类的初始化。

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

便利构造器是类中比較次要的、辅助型的构造器。你能够定义便利构造器来调用同一个类中的指定构造器,并为其參数提供默认值。你也能够定义便利构造器来创建一个特殊用途或特定输入的实例。

你应当仅仅在必要的时候为类提供便利构造器,例如说某种情况下通过使用便利构造器来快捷调用某个指定构造器,可以节省很多其它开发时间并让类的构造过程更清、晰明。

构造器链

为了简化指定构造器和便利构造器之间的调用关系,Swift 採用下面三条规则来限制构造器之间的代理调用:

规则 1

指定构造器必须调用其直接父类的的指定构造器。

规则 2

便利构造器必须调用同一类中定义的其他构造器。

规则 3

便利构造器必须终于以调用一个指定构造器结束。

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

指定构造器必须总是向上代理

便利构造器必须总是横向代理

这些规则能够通过以下图例来说明:

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

子类中包括两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的随意一个,由于它仅仅能调用同一个类里的其它构造器。这满足了上面提到的规则2和3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则1。

注意:

这些规则不会影响使用时,怎样用类去创建实例。不论什么上图中展示的构造器都能够用来完整创建相应类的实例。这些规则仅仅在实现类的定义时有影响。

以下图例中展示了一种更复杂的类层级结构。它演示了指定构造器是假设在类层级中充当“管道”的作用,在类的构造器链上简化了类之间的内部关系。

两段式构造过程

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。

在这个样例中,构造过程从对子类中一个便利构造器的调用開始。这个便利构造器此时没法改动不论什么属性,它把构造任务代理给同一类中的指定构造器。

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

父类中的指定构造器确保全部父类的属性都有值。因为没有很多其它的父类须要构建,也就无需继续向上做构建代理。

一旦父类中全部属性都有了初始值,实例的内存被觉得是全然初始化,而阶段1也已完毕。

父类中的指定构造器如今有机会进一步来定制实例(虽然它没有这样的必要)。

一旦父类中的指定构造器完毕调用,子类的构指定构造器能够运行很多其它的定制操作(相同,它也没有这样的必要)。

终于,一旦子类的指定构造器完毕调用,最開始被调用的便利构造器能够运行很多其它的定制操作。

构造器的继承和重载

跟 Objective-C 中的子类不同,Swift 中的子类不会默认继承父类的构造器。Swift 的这样的机制能够防止一个父类的简单构造器被一个更专业的子类继承,并被错误的用来创建子类的实例。

假如你希望自己定义的子类中能实现一个或多个跟父类同样的构造器--或许是为了完毕一些定制的构造过程--你能够在你定制的子类中提供和重载与父类同样的构造器。

假设你重载的构造器是一个指定构造器,你能够在子类里重载它的实现,并在自己定义版本号的构造器中调用父类版本号的构造器。

假设你重载的构造器是一个便利构造器,你的重载过程必须通过调用同一类中提供的其他指定构造器来实现。这一规则的具体内容请參考构造器链。

注意:

与方法、属性和下标不同,在重载构造器时你没有必要使用keywordoverride。

自己主动构造器的继承

如上所述,子类不会默认继承父类的构造器。可是假设特定条件能够满足,父类构造器是能够被自己主动继承的。在实践中,这意味着对于很多常见场景你不必重载父类的构造器,而且在尽可能安全的情况下以最小的代价来继承父类的构造器。

如果要为子类中引入的随意新属性提供默认值,请遵守下面2个规则:

规则 1

假设子类未定义不论什么指定构造器,它将自己主动继承全部父类的指定构造器。

规则 2

假设子类提供了全部父类指定构造器的实现--无论是通过规则1继承过来的,还是通过自己定义实现的--它将自己主动继承全部父类的便利构造器。

即使你在子类中加入�了很多其它的便利构造器,这两条规则仍然适用。

注意:

子类能够通过部分满足规则2的方式,使用子类便利构造器来实现父类的指定构造器。

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

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

init(parameters) {
statements
}

便利构造器也採用相相同式的写法,但须要在initkeyword之前放置conveniencekeyword,并使用空格将它们俩分开:

convenience init(parameters) {
statements
}

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

接下来的样例将在实战中展示指定构造器、便利构造器和自己主动构造器的继承。它定义了包括三个类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)
}
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实例。这个便利构造器仅仅是简单的将任务代理给了同一类里提供的指定构造器。

注意,RecipeIngredient的便利构造器init(name: String)使用了跟Food中指定构造器init(name: String)同样的參数。虽然RecipeIngredient这个构造器是便利构造器,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。这个类构建了购物单中出现的某一种调味料。

购物单中的每一项总是从unpurchased未购买状态開始的。为了展现这一事实,ShoppingListItem引入了一个布尔类型的属性purchased,它的默认值是false。ShoppingListItem还加入�了一个计算型属性description,它提供了关于ShoppingListItem实例的一些文字描写叙述:

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

注意:

ShoppingListItem未定义构造器来为purchased提供初始化值,这是由于不论什么加入�到购物单的项的初始状态总是未购买。

因为它为自己引入的全部属性都提供了默认值,而且自己未定义不论什么构造器,ShoppingListItem将自己主动继承全部父类中的指定构造器和便利构造器。

你能够使用所有三个继承来的构造器来创建ShoppingListItem的新实例:

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

如上所述,样例中通过字面量方式创建了一个新数组breakfastList,它包括了三个新的ShoppingListItem实例,因此数组的类型也能自己主动推导为ShoppingListItem[]。在数组创建完之后,数组中第一个ShoppingListItem实例的名字从[Unnamed]改动为Orange juice,并标记为已购买。接下来通过遍历数组每一个元素并打印它们的描写叙述值,展示了全部项当前的默认状态都已依照预期完毕了赋值。

通过闭包和函数来设置属性的默认值

假设某个存储型属性的默认值须要特别的定制或准备,你就能够使用闭包或全局函数来为其属性提供定制的默认值。每当某个属性所属的新类型实例创建时,相应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

这样的类型的闭包或函数通常会创建一个跟属性类型同样的暂时变量,然后改动它的值以满足预期的初始状态,最后将这个暂时变量的值作为属性的默认值进行返回。

以下列举了闭包怎样提供默认值的代码概要:

class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型同样
return someValue
}()
}

注意闭包结尾的大括号后面接了一对空的小括号。这是用来告诉 Swift 须要立马运行此闭包。假设你忽略了这对括号,相当于是将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。

注意:

假设你使用闭包来初始化属性的值,请记住在闭包运行时,实例的其他部分都还没有初始化。这意味着你不可以在闭包里訪问其他的属性,就算这个属性有默认值也不同意。相同,你也不能使用隐式的self属性,或者调用其他的实例方法。

以下样例中定义了一个结构体Checkerboard,它构建了西洋跳棋游戏的棋盘:

西洋跳棋游戏在一副黑白格交替的 10x10 的棋盘中进行。为了呈现这副游戏棋盘,Checkerboard结构体定义了一个属性boardColors,它是一个包括 100 个布尔值的数组。数组中的某元素布尔值为true表示相应的是一个黑格,布尔值为false表示相应的是一个白格。数组中第一个元素代表棋盘上左上角的格子,最后一个元素代表棋盘上右下角的格子。

boardColor数组是通过一个闭包来初始化和组装颜色值的:

struct Checkerboard {
let boardColors: Bool[] = {
var temporaryBoard = Bool[]()
var isBlack = false
for i in 1...10 {
for j in 1...10 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
return boardColors[(row * 10) + column]
}
}

每当一个新的Checkerboard实例创建时,相应的赋值闭包会运行,一系列颜色值会被计算出来作为默认值赋值给boardColors。上面样例中描写叙述的闭包将计算出棋盘中每一个格子合适的颜色,将这些颜色值保存到一个暂时数组temporaryBoard中,并在构建完毕时将此数组作为闭包返回值返回。这个返回的值将保存到boardColors中,并能够通squareIsBlackAtRow这个工具函数来查询。

let board = Checkerboard()
println(board.squareIsBlackAtRow(0, column:1))
// 输出 "true"
println(board.squareIsBlackAtRow(9, column:9))
// 输出 "false"