本文基本涵盖 Swift 中关于构造器的所有基础知识点,错误难免,希望各位指正。
基本使用
“Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization that is required before the new instance is ready for use.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
这是官方的定义,从中可以看到:
- 构造器可以使用的场景不仅仅是类,还可以是结构体和枚举(我们知道 Swift 中的结构体和枚举的强大已经不是 Objective-C 可以相比的了);
- 实例在构造完成之前是无法使用的;
- 构造过程主要是包括存储型属性的赋初值的过程,另外还包括一些其他必须的设置和初始化工作;
为了阅读的通畅,我们先展示类、结构体、枚举的构造过程,这样可以有一个初步的认识。之后再进一步深入。
类的构造过程
1 | class Person { |
这里定义一个 Person
类,拥有两个属性,name
(因为名字可以更改,所以是 var
,又因为所有人都要有名字,所以是非可选型)和 id
(ID 一般是无法更改的,所以是 let
)。Swift 中的构造器没有返回值,在其中我们完成了两个属性的初始化。
属性是可以赋默认值的,没有赋默认值的属性必须在构造器中完成初始化;赋默认值的属性如果没有在构造器中完成初始化,构造过程会用其默认值为其初始化。
上面代码我们暂且让 id
可更改,我们为其设置一个默认值 "000000"
:
1 | class Person { |
可以看到,构造过程中我们只初始化了一个属性,有默认值的属性如果在这个过程中没有被初始化,那么会自动通过默认值为其初始化。
类只有一种情况下会提供默认的构造器,就是这个类的所有属性均存在默认值,并且还没有自定义构造器的情况,这个默认的构造器就是无参数的构造器。
1 | class Person { |
我们把两个属性均设置了默认值,那么即使我们没有提供构造器,也是可以通过 Person()
构造出一个实例,这个实例的所有属性全是默认值。
结构体的构造过程
1 | struct Location { |
可以看到,结构体中我们没有设置构造器,但还是使用了一个全参数的构造器完成了构造过程。
如果没有自定义构造器,结构体会默认提供一个包含全部属性作为参数的构造器;如果结构体的所有属性均设置了默认值,那么还会额外提供一个无参数的构造器。
1 | struct Location { |
可以看到,我们通过无参数或者是全参数的构造器,都构造出了一个实例。
需要说明的是,即使部分属性已经设置了默认值,那么系统默认提供的还是一个全参数的构造器。
枚举的构造过程
1 | enum Rating { |
通常情况下,枚举变量可以通过 类型名.case名
的形式构造,这也是绝大多数的使用方式,不过 Swift 中我们仍然可以为枚举添加构造器。上面的例子中,我们通过得分来构造一个表示等级的枚举变量。最终两种构造方式都可以构造出一个枚举变量/常量。
需要说明的是,这个构造器是个可失败的构造器(它的语法就是在 init
后面增加一个问号),也就是允许构造失败(当传入的分数不在 0 到 100 的范围内的时候就构造失败),因此 rating1
的类型是 Rating
而 rating2
的类型是 Rating?
,即它是一个可选型,使用时需要解包。
进一步理解
自定义构造器
显然我们无法只使用系统提供的构造器,况且很多情况下系统并不为我们提供构造器,这就需要我们自定义构造器。
上述举例中其实已经涉及了自定义构造器,不过都是最基本的直接为属性赋值的方式,下面的例子可以看出,构造过程也可能存在更多的逻辑。
1 | class Person { |
这个例子中我们把传入的两个参数值通过一定的逻辑后赋值给 name
属性。
一旦自定义了构造器,那么系统将不再提供任何默认的构造器,即使属性都存在默认值。类和结构体都是如此。
需要说明的一点是,这个例子中,构造器中使用的是 name
而非前面的 self.name
,这是完全没问题的,前面的例子是因为参数变量的名字和属性名同名,那么在函数作用域内部,局部变量就占用了这个名字了。
构造器参数的默认值
在 Swift 中,构造器和普通函数一样,参数是可以提供默认值的
1 | class Person { |
如果参数设置了默认值,那么系统会额外奉送一个构造器,那就是省略了所有默认参数的版本,上面的例子中,我们仅仅通过 age
作为参数也构造出了一个实例。
可失败的构造器
上面例子中,枚举的例子就是一个可失败的构造器,也就是允许返回 nil
,响应的返回类型也是一个可选型。其语法很简单,就是 init
后面增加问号。
在 Swift 标准库中,可失败的构造器有很多
1 | let num1 = Int("123") |
这是我们很常见的把字符串转为整型的方式,Swift 是通过 Int 的一个可失败的构造器完成的,因为很显然,不是所有的字符串都可以转成整型的。
1 | enum Direction: Int { |
有原始值的枚举通过原始值创建枚举变量的构造器也是一个可失败的构造器,上面的例子中,不是所有整数都能对应一个方向的。
可失败的构造器的可失败性(暂且这么称呼吧)是可传递的,比如传递到便利构造器中(便利构造器后文会重点讲解)或者子类的构造器中。
1 | class Person { |
这个例子中,我们假定 name
只能是中国人名,我们简单的限制字符数只能不大于 4,否则构造失败,那么 Student
类的构造器由于调用了父类的可失败的构造器,那么这个构造器就也要是一个可失败的构造器才行。
还有一种解决方式是处理最终构造出来的对象比如强制解包(super.init(name: name)!
),但这显然是不推荐的。
属性是可选型的情况下的构造过程
很多人一开始学习 Swift 时不理解为什么可选型属性可以不用初始化,只好死记硬背,其实这很好理解,只需要记住一句话:
可选型属性即使只声明,其也有默认值,它的默认值是 nil
也就是说,可选型属性即使只声明,其也具有默认值,这样就解释了为什么其“不用初始化”,实质上它和其他拥有默认值的属性一样,如果没有在构造器中初始化,那么构造过程就会默认用 nil 将其初始化。可选型本质上是枚举,nil 只是它的一个值而已。
1 | class Person { |
类的两段式构造
基本介绍
类因为有继承的特性,构造器不仅仅要完成本类属性初始化这么简单,它还要考虑父类继承来的属性如何初始化,以及其他逻辑的实现,于是就有了两段式构造的特征。
1 | class Person { |
这个例子中,子类的构造器里我们做了三件事:
- 为本类的属性初始化
- 通过调用父类构造器完成从父类继承的属性的初始化
- 为父类中已经提供默认值的属性初始化(当然这一步还可以做更多其他逻辑,可以调用实例方法)
“Notice that the initializer for the EquilateralTriangle class has three different steps:
- Setting the value of properties that the subclass declares.
- Calling the superclass’s initializer.
- Changing the value of properties defined by the superclass. Any additional setup work that uses methods, getters, or setters can also be done at this point.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
其中前两步就是第一段的构造过程,这一段实质上已经完成了实例的构造,第一段构造构造完成,这个实例就已经可以使用了。
那么为什么还要第二段构造呢?我们可以看上面的例子,这个例子中,父类继承的属性 id
如何在子类中初始化呢?首先我们要知道父类继承来的属性必须通过调用父类构造器完成初始化,可是此时父类提供的构造器是把 id
属性按照其默认值进行初始化的,也就是说,在子类中,我们最初构造出的这个实例,其 id
属性只能是它的默认值 000000
,那么我们在子类还希望按照自己的意愿为其赋初值啊?那么就应该在第二段构造过程中进行,这一段构造过程本质上其实并不再是赋初值的过程,而是更改值的过程(之前写短文讨论过属性观察器的触发时机,第二段构造中属性的赋值已经可以触发属性观察器了)。
额外说明一个小地方,这里 super
调用父类构造器是必须进行的吗?答案是是的!但还是有可以省略的情况的(省略不写不代表没有调用),但这既少见也不建议,比如下面:
1 | class Person { |
Swift 中类的构造过程与 Objective-C 的对比
很多程序员从 Objective-C 转到 Swift 后,都会有一个疑惑。
为什么 Objective-C 中 都是先调用 super
,再为属性赋值,而 Swift 中却是颠倒,必须先为属性赋值,才能调用 super
呢?
阅读了前文相信不少人已经有了答案。我们先来看看 Objective-C 一般是怎么做的:
1 | // QWPPerson 类 |
通常我们都是这样写的,很多文章里的解释是,我们先调用 super
完成父类的初始化,然后再进行本类的初始化。
我们对比一下 Swift,发现什么了么?其实很简单,这里所谓的“父类的初始化”,就是 Swift 中的第一段的构造过程,只不过 Objective-C 中所有属性都有默认值,那就是 0(nil 也是 0)。所以第一阶段构造实例的过程完全是把所有属性按照默认值去初始化构造出了这个实例,而所谓的“本类的初始化”就很像 Swift 中的第二段构造过程,这里实例已经创建(如果 if 判断成立的话),之后在里面其实是修改属性的值而已,从 0 修改成一个非 0 的值。
也就是说 Swift 的构造过程相对更灵活,它可以不必一开始只能用默认值去构造这个实例。所以 Swift 的构造过程并不是说把 super
交换到了后面,而是在 super
前面增加了更多的灵活性。
“Swift’s two-phase initialization process is similar to initialization in Objective-C. The main difference is that during phase 1, Objective-C assigns zero or null values (such as 0 or nil) to every property. Swift’s initialization flow is more flexible in that it lets you set custom initial values, and can cope with types for which 0 or nil is not a valid default value.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
对于 Objective-C,我们可以模仿 Swift 尝试一种从没写过的方式:
1 | - (instancetype)initWithName:(NSString *)name andStudentNumber:(NSInteger)studentNumber { |
我们把 studentNumber
的属性初始化提前到 super
调用之前尝试一下,结果仍然可以成功创建实例。
小结
不管是默认提供的构造器,还是自定义的构造器,还是说类的两段式构造方式,其核心其实都在做一件事,那就是必须保证所有的属性都初始化!只有所有属性都初始化,这个实例才算构造完成,才可以使用。
值类型的构造器代理(Initializer Delegation)
“Initializers can call other initializers to perform part of an instance’s initialization. This process, known as initializer delegation, avoids duplicating code across multiple initializers.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
名字很唬人,但其实就是一个构造器中可以调用另一个构造器。
这一节我们只讨论值类型的,也就是不考虑类,我们以结构体为例。
1 | struct Size { |
通过 self
我们可以在一个构造器中调用另一个构造器,这简化了一些重复代码。
相信很多了解 Swift 的同学会发现,这和类中的便利构造器很像啊。是的,便利构造器也是一种构造器代理,只不过由于结构体没有继承的特性,那么这里的构造器不用区分什么便利不便利,大家都一样,不用加 convenience
关键字。
下面我们就逐步开始讲解 Swift 构造过程中较难的地方了,我们就从便利构造器开始。
类的继承和构造过程
便利构造器和指定构造器
首先要确定,便利构造器和指定构造器的概念只存在于类中,值类型是不做区分的。
便利构造器也是构造器代理的一种情况。
类中的非便利构造器就是指定构造器。
便利构造器中仍然可以调用另一个便利构造器,这是没有问题的,但是最终都会去调用一个指定构造器。
我们只提便利构造器的话,它也很简单,和上面值类型的情况差不多。
1 | class Person { |
可以看到,和值类型不同的是,这里仅仅是多了一个 convenience
关键字,其他好像没什么区别,同样是增加了一个新的构造器。
那还为什么要加这个关键字呢?那是因为类是有继承的特性的,增加了这个维度,很多东西就复杂起来啦。
构造器代理规则在类中的表现
前面我们了解了构造器的代理,其实就是一个构造器可以调用另一个构造器嘛,但是在加入继承这一维度之前,它们都是在调用本类中的其他构造器而已。
再往前面的例子中,我们有继承的例子,那些例子中我们在子类的构造器中通过 super
关键字调用了父类的构造器,事实上这也是构造器代理。
那么在类中,构造器代理满足什么样的规则呢?
Rule 1
A designated initializer must call a designated initializer from its immediate superclass.
Rule 2
A convenience initializer must call another initializer from the same class.
Rule 3
A convenience initializer must ultimately call a designated initializer.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
总结下来就是三点:
- 指定构造器只能调用父类的指定构造器(注意无法调用父类的便利构造器哦)
- 便利构造器只能调用本类中的其他构造器(便利构造器中无法使用
super
关键字而只能使用self
) - 便利构造器最终一定会调用到一个指定的构造器(也就是说实例的构建本质上还是在指定构造器中构建的,便利构造器没有构造实例的能力,需要最终依靠指定构造器)
下面这张图形象地解释了这种关系:
图 1 - 构造器代理
这样又我们可以把上面的三点总结为两点:
- 指定构造器只能纵向调用
- 便利构造器只能横向调用
1 | class Person { |
这个例子中,就完全遵守这样的规则。
构造器的继承和重写
构造器的继承
1 | class Person { |
这里的父类我们仍然定义一个 Person
类,拥有一个指定构造器和一个便利构造器;接下来我们设置一个子类
1 | class Student: Person { |
为了测试构造器的继承,我们在这个子类中什么构造器都没写,只增加了一个具有默认值的属性;结果会怎么样呢?结果就是,我们可以成功使用前面的两个构造器构造出 Student
实例:
1 | let harry = Student(name: "Harry", age: 11) |
是的,我们成功从父类继承了他的所有构造器,一个指定构造器,一个便利构造器。
我们总结一下上面代码有什么特征:
- 子类的所有属性都提供了默认值
- 子类没有定义任何指定构造器(当然也不会定义便利构造器)
带来的结果呢:
- 子类继承了父类所有构造器,包括指定构造器和便利构造器
现在我们开始改变了,首先我们尝试去掉子类属性 studentNumber
的默认值;结果报错了!提示 Class 'Student' has no initializers
,它告诉我们 Student
类没有构造器!刚才我们还继承了两个构造器,现在一下子一个也没有了。
我们发现子类因为含有没有赋默认值的属性,父类的所有构造器就不再供子类继承了。这是为什么呢?答案很简单,因为父类的构造器无法完成子类属性的初始化。父类并不知道子类有什么属性,我们假设此时可以使用父类的构造器完成构造过程,那么子类的这个 studentNumber
属性应该被初始化成什么呢?
于是我们只好自己自定义构造器了
1 | init(name: String, age: Int, studentNumber: String) { |
我们创建一个构造器,然后尝试创建实例,通过代码提示可以看到,此时子类只有一个构造器,就是我们新创建的这个构造器。父类的构造器仍然没有被继承。
于是突然想到了父类方法是可以重写的,那么父类的构造器是否也能重写呢?答案是是的,我们此时可以重写父类的指定构造器。
1 | // 前面加 override 关键字实现对父类构造器的重写 |
现在我们自己定义了一个,然后重写一个父类的,那么我们现在应该有两个了吧!我们尝试一下:
1 | let harry = Student(name: "Harry", age: 11, studentNumber: "123456") |
我们竟然拥有了三个构造器!原来父类的便利构造器现在又被继承了!原因是:
- 如果子类提供了所有父类指定构造器的实现,那么子类将继承父类所有的便利构造器
这样,我们基本就了解了构造器的继承原则,它最终可以总结成下面两句话:
- 当我们没有实现任何自定义构造器的情况下,子类继承所有父类指定构造器和便利构造器(当然这种情况一定是子类所有属性都有默认值的情况,不然会报错)
- 当我们重写了所有父类指定构造器的情况下,子类额外继承父类的所有便利构造器
当然也可以参考官方的说法,大同小异:
Assuming that you provide default values for any new properties you introduce in a subclass, the following two rules apply:
Rule 1
If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.
Rule 2
If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.”
Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.
构造器的重写
我们刚才注意到了,构造器是可以重写的,但是我们只重写了指定构造器。那么就有疑问了,便利构造器是否可以重写呢?答案是否定的,便利构造器无法被重写。但是有的同学却是看到过 override convenience init
的字眼出现,这是怎么回事呢?
这是因为重写的父类指定构造器可以是便利构造器,但是便利构造器无法被重写!这里有些绕,我们拿官方的例子说明。
1 | class Food { |
它的结构如下图所示:
图 2 - 构造器重写1
1 | class RecipeIngredient: Food { |
我们此时又定义一个 RecipeIngredient
类,这个类中有一个自定义的构造器,它通过调用父类指定构造器完成构造;同时还有一个便利构造器,这个构造器通过调用本类的指定构造器完成构造;巧合的是,这个便利构造器和父类的那个指定构造器长的一模一样!于是没办法,它既是一个便利构造器,又完成了父类指定构造器的重写,所以又加上了 override
关键字。
目前这两个类的结构如下图所示:
图 3 - 构造器重写2
子类并不是重写了一个父类的便利构造器,而是通过便利构造器的方式完成了对父类指定构造器的重写。
于是我们又满足了上面的第二条原则,我们重写了所有父类的指定构造器,于是父类的所有便利构造器也被子类继承,现在子类拥有三个构造器。
required 构造器
构造器加上 required 关键字代表子类必须实现
1 | class Person { |
对于 required 的构造器, 重载不需要再写 override, 而是还写 required。