Swift 构造函数

初始化(Initilization)是设置类型的实例的操作。它为每个存储属性给定了一个初始值, 并且可能会牵涉其它准备工作。这个处理之后, 这个实例就准备好了并且可用了。

初始化时, 属性的值要么是给定的默认存储值, 要么是根据需要计算得到的值。

初始化函数的语法


结构体和类要求在初始化完成后, 它们的存储属性拥有初始值。这个要求解释了为什么你一直给你所有的存储属性设置默认值。如果你还没给你的这些存储属性默认值, 那么编译器就会给你报错并告诉你该类型的属性还没有被准备好使用。在类型上定义一个初始化函数是另外一种保证在实例被创建之后属性有值的方式。

初始化函数的定义和你看过的函数有点不一样, 初始化函数不是以 func关键字开头, 尽管它也是类型中的方法。初始化函数的语法看起来像这样:

1
2
3
4
5
struct CustomType {
init(someValue: SomeType) {
// 这儿是初始化代码
}
}

这个通用的语法在结构体、枚举和类之间没什么不同。在上面的例子中, 初始化函数有一个叫做 someValue类型为 SomeType 的参数。而初始化函数通常有一个或多个参数, 它们也可以含有 0 个 参数。(这时 init关键字后面有一组空括号)

不像其它方法, 初始化函数不返回值。相反, 初始化函数的任务是给类型的每个存储属性设定上值。

结构体初始化


结构体即可以有默认初始化函数又可以有自定义初始化函数。当你使用结构体的时候, 你通常会利用提供好的默认初始化函数, 但是也有其它你会自定义初始化处理的情况。

结构体的默认初始化函数


还记得你是怎么获得你的Town类型的实例的吗? 你给Town类型的存储属性设置了默认值。你不知道的是你使用了一个由 Swift 自动提供的空的初始化函数。(一个不含参数的初始化函数)。 当你键入像 var myTown = Town() , 那么这个语法就会调用空的初始化函数并给新的实例的属性设置上你指定的默认值。

另外一种形式的默认初始化函数就是逐个成员初始化函数(memberwise initializer)。逐个成员初始化函数中类型的每个存储属性都有一个参数。 这时, 你不会让编译器根据你指定的默认值来填充新的实例的属性的值。相反, 免费的逐个成员初始化函数会包含所有需要值的存储属性。(我们称之为免费的, 是因为它是 Swift 编译器自动提供的 — 你不需要定义它)。

记住, 初始化的一个准则就是给新的实例的所有存储属性设置上值以准备使用。编译器会强制要求你的新的实例中的所有存储属性都有值。如果你没有为你的自定义的结构体提供初始化函数, 你必须通过默认值或逐个成员初始化函数提供必要的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用逐个成员初始化函数
struct Town {
var population = 5422
var numberOfStoplights = 4

// 实例方法
func printTownDescription() {
print("Population: \(population); number of stoplights: \(numberOfStoplights)")
}
}

var myTown() = Town(population: 10000, numberOfStoplights: 6)
myTown.printTownDescription()

注意, Town 的属性 populationnumberOfStoplights 有默认值。 这些默认值不同于你提供给 Town 的逐个成员初始化函数的参数值。现在打印出:

1
Population: 10000; number of stoplights: 6

这些属性的值是怎么改变默认值的呢?

myTown这个实例现在是用免费的逐个成员初始化函数创建的。这个 Town 类型的所有存储属性列出在初始化函数里面, 它允许你为实例属性指定新的值。就像你看到的, 你传给初始化函数的新值替换掉了默认的值。

注意, Town 的属性名被用作了该初始化函数的外部参数名。Swift 自动地为每个初始化函数提供默认的外部参数名, 每个参数一个。这个约定很重要, 因为 Swift 中所有的初始化函数都含有相同的名字: init。因此, init 这个函数名不能用于识别应该调用哪个指定初始化函数。参数的名字加上参数的类型才能帮助编译器区分不同的初始化函数, 以至于它能知道该调用哪个初始化函数。

默认的结构体的逐个成员初始化函数是有用的, 因为 Swift 自动为你提供了它们。你免费获得了它们。结构体的这个优点让结构体特别吸引人。然而, 通常你想自定义你的类型的初始化函数。这就轮到自定义初始化函数了。

为结构体自定义初始化函数


现在轮到你为 Town 类型写你自己的初始化函数了。自定义的初始化函数很强大, 而越强大则责任越多。当你写了自己的初始化函数时, Swift 就不会给你免费的初始化函数了(和默认的逐个成员初始化函数告别把)。你要为自己确保实例的所有属性都有给定的合适值负责。

现在打扫下房间, 把属性的默认值移除掉(默认值只是告诉你每个实例的属性都要有值, 现在该我们自己动手实现了)。

1
2
3
4
5
6
7
8
9
struct Town {
let region: String
var population: Int
var numberOfStoplights

func printTownDescription() {
print("Population: \(population); number of stoplights: \(numberOfStoplights); region: \(region)")
}
}

现在是时候创建你的自定义初始化函数了。之后, 你会从这个类型中定义的其它初始化函数调用这个初始化函数。现在, 为你的 Town 类型添加如下初始化函数。

1
2
3
4
5
6
7
8
// 添加一个逐个成员初始化函数
...
var numberOfStoplights: Int
init(region: String, population: Int, stoplights: Int) {
self.region = region
self.population = population
numberOfStoplights = stoplights
}

init(region:population:stoplights:) 方法接收了3个参数, 每一个参数都是为 Town 类型中的存储属性设置新值。你接收初始化函数的参数的值并把它们传递给类型实际的属性。例如, 传递给初始化函数的 region参数的值被设置为 region属性的值。因为初始化函数中的参数名和属性的名字相同, 你需要通过 self显式地访问该属性。numberOfStoplights属性就没有这个问题, 所以你仅仅把初始化函数的 stoplights参数的值设置给 numberOfStoplights属性。

注意, 你给 region 属性设置了值, 即使它被声明为一个常量。Swift编译器允许你在初始化期间的某个点那儿初始化一个常量属性。记住, 初始化的目的是为了保证类型的属性在初始化完成后都有值。

之前编译器给你提供的免费逐个成员初始化函数的参数使用是实际的属性名 numberOfStoplights。在 Town 这个初始化函数中, 你把参数名缩短为 stoplights

1
var myTown = Town(region: "West", population: 10000, stoplights: 6)

初始化函数的代理


你可以在同一个类型中定义一个初始化函数来调用其它初始化函数。这个过程叫做初始化函数的代理。它通常用于为创建类的实例时提供多个路径。

在值类型中(例如枚举和结构体), 初始化函数代理相对直接。因为值类型不支持继承, 初始化函数代理只涉及调用类型定义的其它初始化函数。对于类来说就有点复杂了。你很快就会看到。

1
2
3
4
5
6
7
8
9
10
init(region: String, population: Int, stoplights: Int) {
self.region = region
self.population = population
numberOfStoplights = stoplights
}

// 初始化函数代理(它自己做不了, 但是通过别人来完成初始化)
init(population: Int, stoplights: Int) {
self.init(region: "N/A", population: population, stoplights: stoplights)
}

你在self上调用了其它初始化函数。你传递了 population 和 stoplights 参数, 但是没有 region 参数, 所以你必须提供自己 region 值, 这儿, 你指定了字符串”N/A”.

初始化函数代理能帮助减少代码的重复。

这就是为什么我们说初始化函数代理“定义路径”。

因为你定义了你自己的逐个成员初始化函数, 编译器不会再给你提供免费的初始化函数。这不是有所限制, 它甚至是一种优点。例如, 你可能想使用这个新的初始化函数, 如果给定的 town 没有 region 信息可以获得时。那样, 你会使用你创建的新的带有参数的初始化函数为 population 和 stoplights 设置对应的属性而给 region一个占位符值。

1
2
3
// 使用初始化函数代理
var myTown = Town(population: 10000, stoplights: 6)
myTown.printTownDescription()

类的初始化


类的继承给初始化增添了某些复杂性。

特别的地方是, 类添加了指定初始化函数和便利初始化函数的概念。类中的初始化函数要么是指定初始化函数, 要么是便利初始化函数。指定初始化函数负责确保实例的所有属性在初始化完成之前都有值, 因此让实例可以准备好被使用。

便利初始化函数是辅助指定初始化函数的。它们通过调用类的指定初始化函数来补充指定初始化函数。便利初始化函数的角色通常是创建特定用途的类的实例。

类的默认初始化函数


如果你为所有属性提供默认值并且不写你自己的初始化函数, 那么类会获得一个默认的, 的初始化函数。类不会像结构体那样获得一个免费的逐个成员初始化函数。这解释了之前你为什么给类设置默认值: 它允许你利用免费的空初始化函数。因此, 你能获得 Zombie 类的实例, 像这样: let fredTheZombie = Zombie(), 空的圆括号表明你正在使用默认的类初始化函数。

初始化和类的继承


1
2
3
4
5
6
7
8
class Monster {
var town: Town?
var name: String // 如果不设置初始值, 就得指定变量的类型
init(town: Town?, monsterName: String) {
self.town = town
name = monsterName
}
}

这个初始化函数有两个参数: 一个是 Town类型的 optional 实例, 另外一个是 monster 的名字。在初始化函数的实现中, 这些参数的值被赋值给类的属性。 再一次, 注意初始化函数中的参数 town 于类的属性名town 匹配, 所以你必须通过 self访问该属性并给它赋值。但是你不必通过 self访问 name属性, 因为初始化函数的参数有一个不同的名字。

自动初始化函数继承


类通常不继承它们的父类的初始化函数。Swift 的这个特征的目的是阻止子类无心地提供一个初始化函数但是却没有为子类的所有属性设置上值, 因为子类中常常添加父类中不存在的额外属性。要求子类拥有自己的初始化函数帮助防止使用不彻底的初始化函数进行部分初始化。

然而, 存在类确实自动继承它的父类的初始化函数的情况。如果你的子类为添加的所有新的属性提供了默认值, 那么存在两个类会继承它的父类的初始化函数的场景。

  • 如果子类没有定义任何指定初始化函数, 它会继承它的父类的指定初始化函数
  • 如果子类实现了父类所有的指定初始化函数 — 不管是显式的还是通过继承的, 它会继承它的父类的所有便利初始化函数。

你的Zombie 类型就是适应于第一个场景。它继承了 Monster类型的唯一的指定初始化函数, 因为它为它添加的所有属性提供了默认值并且它没有定义它自己的指定初始化函数。这个初始化函数的签名是 init(town:monsterName:)。还有, 因为 Zombie 类型继承了一个初始化函数, 编译器不再提供你之前使用过的免费初始化函数。

因此, 从编译器的角度来看, Zombie 类没有一个空的初始化函数可用。并且, 仅此作答, Zombie 类的初始化函数缺少 town 这个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Zombie: Monster {
var walksWithLimp = true

// final 用来防止子类重写父类的方法, 僵尸的行为都是一致的。子类不能改写
final override func terrorizeTown() {
town?.changePopulation(-10) // 僵尸一出来就吃掉 10 个人, town 属性继承自父类 Monster
super.terrorizeTown() // super 关键字专门用于继承, 所以枚举和结构体中没有。
}
func changeName(name: String, walksWithLimp: Bool) {
self.name = name
self.walksWithLimp = walksWithLimp
}
}

let fredTheZombie = Zombie(town: myTown, monsterName: "Fred")

现在, 当你创建 MonsterZombie 类型的一个实例时, 你给该实例的 town 和 name 属性赋上值。

类的指定初始化函数


类使用指定初始化函数作为它们的主初始化函数。作为这个角色的一部分, 指定初始化函数负责确保在初始化结束之前该类的属性都被赋了值。如果类拥有一个父类, 那么它的指定初始化函数必须调用它的父类的指定初始化函数。

你已经为 Monster 类写了一个指定初始化函数。 回忆下前面:

1
2
3
4
init(town: Town?, monsterName: String) {
self.town = town
name = monsterName
}

指定初始化函数是未经装饰的, 意思是指定初始化函数的 init 关键字前面没有特殊的关键字。这个语法区分了指定初始化函数和便利初始化函数, 便利初始化函数的 init 关键字前面使用了关键字 convenience

Monster 类的初始化函数保证了在初始化结束前它的所有属性都给定了值。 目前, Zombie 类型为它的所有属性(除了继承自 Monster 的那些属性)给定了默认值。因此, 你为 Monster 类定义的初始化函数用在 Zombie 里面也工作的很好。然而, 如果 Zombie 定义了它自己的初始化函数的话会更好, 以至于你能自定义它的初始化函数。

现在移除 Zombie 类的属性的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Zombie: Monster {
var walksWithLimp: Bool
private(set) var isFallingApart: Bool

// final 用来防止子类重写父类的方法, 僵尸的行为都是一致的。子类不能改写
final override func terrorizeTown() {
town?.changePopulation(-10) // 僵尸一出来就吃掉 10 个人, town 属性继承自父类 Monster
super.terrorizeTown() // super 关键字专门用于继承, 所以枚举和结构体中没有。
}
func changeName(name: String, walksWithLimp: Bool) {
self.name = name
self.walksWithLimp = walksWithLimp
}
}

移除掉这些默认值触发了编译器错误: 类 Zombie 没有初始化函数。如果没有赋值默认的值, Zombie 类需要一个初始化函数来在初始化完成前给它的属性赋上值。

Zombie 类添加一个新的初始化函数来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Zombie: Monster {
override class var spookyNoise: String {
return "Brains..."
}
var walksWithLimp: Bool
private(set) var isFallingApart: Bool

init(limp: Bool, fallingApart: Bool, town: Town?, monsterName: String) {
walksWithLimp = limp
isFallingApart = fallingApart
super.init(town: town, monsterName: monsterName)
}

final override func terrorizeTown() {
if !isFallingApart {
town?.changePopulation(-10)
}
super.terrorizeTown()
}
}

你的新的初始化函数消除了错误, 因为你现在保证了 Zombie 的属性在初始化结尾时都有值了。 这里你添加了两部分。首先, 新的初始化函数通过 limpfallingApart 参数给 walksWithLimpisFallingApart属性设置了值。这俩个属性是Zombie 类所特有的, 所以指定初始化函数使用合适的值初始化了它们。

其次, 你调用了Zombie 的父类的指定初始化函数。就像第 15 章那样, super指的是子类的父类。因此, 语法 super.init(town: town, monsterName: monsterName)Zombie 类的初始化函数中的 townmonsterName 参数的值传递给 Monster 类的指定初始化函数。这样做就调用了Monster 类的初始化函数, 它会确保 Zombie 中的 townname 属性被设置上值。查看图17.3:

img

你可能想知道为什么在最后调用父类的初始化函数。因为 Zombie 的初始化函数是 Zombie 类的指定初始化函数, 它负责初始化它所引入的所有属性。这些属性被赋值之后, 子类的指定初始化函数负责调用它的父类的初始化函数以使父类能初始化它的属性。(先做好自己的, 再去要求别人。)

1
2
3
4
5
6
7
8
// let fredTheZombie = Zombie(town: myTown, monsterName: "Fred")
// 替换为
let fredTheZombie = Zombie(
limp: false,
fallingApart: false,
town: myzTown,
monsterName: "Fred"
)

类的便利初始化函数


不像指定初始化函数, 便利初始化函数并不负责确保类的所有属性都有值。相反, 它们仅仅处理它们被定义去做的那部分工作, 然后把信息传递给其它便利初始化函数或指定初始化函数。所有的便利初始化函数在同一个类中调用其它的初始化函数。最终, 便利初始化函数必须调用到类的指定初始化函数那儿。给定类中便利初始化函数和指定初始化函数的关系:类的存储属性接收初始化值, 定义一个路径。

Zombie 类型上弄一个便利初始化函数。它会省略 town 和 monsterName 参数, 意思是这个初始化函数的调用者将会只需要为这个初始化函数的参数提供参数负责。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用便利初始化函数
...
init(limp: Bool, fallingApart: Bool, town: Town?, monsterName: String) {
walksWithLimp = limp
isFallingApart = fallingApart
super.init(town: town, monsterName: monsterName)
}
convenience init(limp: Bool, fallingApart: Bool) {
self.init(limp: limp, fallingApart: fallingApart, town: nil, monsterName: "Fred")
if walksWithLimp {
print("This zombie has a bad knee.")
}
}
final override func terrorizeTown() {
if !isFallingApart {
town?.changePopulation(-10)
}
} ...

你使用 convenience关键字把一个初始化函数标记为便利初始化函数。这个关键字告诉编译器该初始化函数将会需要代理给该类的其它初始化函数, 最终调用一个指定初始化函数。这次调用之后, 该类的实例将会能够使用。

然而, 该便利初始化函数在 Zombie 类上调用指定初始化函数。它传递形参接收到的值: limpfallingApart。 对于便利初始化函数没有接收到的形参的值, townmonsterName, 你传递了一个 “nil” 和 “Fred” 给 Zombie 的指定初始化函数。

一旦便利初始化函数调用了指定初始化函数, 那么该类的实例就完全准备好使用了。因此你可以检查实例的 walksWithLimp 属性的值。如果你尝试在调用 Zombie 的指定初始化函数之前检查这样做, 那么编译器就会报错: 在 self.init 被调用之前, 代理初始化函数中使用了 self。这个错误告诉你代理初始化函数正尝试使用 self, 而它需要访问 walksWithLimp属性, 在它被准备好使用之前。

img

你现在可以使用这个便利初始化函数创建Zombie 类型的实例。

1
let convenientZombie = Zombie(limp: true, failingApart: fasle)

类的必须的初始化函数


类可以要求它的子类提供指定的初始化函数。例如, 假如你想要所有的 Monster 子类提供 monster 的 name 值和 town 值(或者 nil, 如果没有找到一个 town 的话)。要这样的话, 使用 required关键字来标记一个初始化函数以让给类型的子类必须提供指定的初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使 town 和 monsterName 是必须的
class Monster {
var victimPool: Int {
...
}
required init(town: Town?, monsterName: String) {
self.town = town
name = monsterName
}
func terrorizeTown() {
...
}
}

Monster 类的唯一指定初始化函数现在是必须的。子类必须实现这个初始化函数。

这样的话子类中会报错, 因为你还未在Zombie子类中实现这个新要求的初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在 Zombie.swift 中添加必须的初始化函数
...
convenience init(limp: Bool, fallingApart: Bool) {
self.init(limp: limp, fallingApart: fallingApart, town: nil, monsterName: "Fred")
if walksWithLimp {
print("This zombie has a bad knee.")
}
}

required init(town: Town?, monsterName: String) {
walksWithLimp = flase
isFallingApart = false
super.init(town: town, monsterName: monsterName)
}

final override func terrorizeTown() {
if !isFallingApart {
town?.changePopulation(-10)
}
super.terrorizeTown()
}
...

要实现一个父类的必须要求的初始化函数, 在子类的初始化函数前置一个 required关键字。 不像其它函数那样, 如果是继承自你的父类必须重写, 你不必用 override关键字标记初始化函数。它由 required关键字隐式的标记了。

你的这个 required初始化函数的实现让它成为 Zombie 类的一个指定初始化函数。为什么? 你问? 好问题。

回想指定初始化函数负责初始化类型的属性,还负责向上代理给父类的初始化函数。这个实现正是做了那两件事情。你因此能够使用这个初始化函数来实例化Zombie 类。

这个时候, 你可能想知道, Zombie 有多少个指定初始化函数?答案是两个。init(limp:fallingApart:town:monsterName:)init(town:monsterName)。有多个指定初始化函数是完全可以的, 并没有什么不寻常。

Deinitialization


反初始化是从内存中移除类的实例的部分处理, 当它们不再需要时。从概念上讲, 它是初始化的对立面。反初始化被限于引用类型。这对值类型不可用, 因为当它们从作用域中移除后就从内存中移除了。

在 Swift 中, 反初始化函数在实例被移出内存前被立即调用。在实例被释放内存之前它提供了一个机会用于做任何最后的维护。

一个类只有一个反初始化函数。反初始化函数以init关键字开头, 不接收任何参数。让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
required init(town: Town?, monsterName: String) {
walksWithLimp = false
isFallingApart = false
super.init(town: town, monsterName: monsterName)
}
...
func changeName(name: String, walksWithLimp: Bool) {
... }
deinit {
print("Zombie named \(name) is no longer with us.")
}
...

注意反初始化函数访问了Zombie 的name 属性。 反初始化函数可以访问到实例的全部属性和方法。

1
fredTheZombie = nil