深入了解Swift中的初始化

Swift version:5.0

初始化是什么

初始化简而言之是一个准备的过程,就好比你想吃地三鲜,这时候你光在脑海里想,你是吃不到的,你需要买菜、洗菜、切菜、炒菜,然后你才能吃上地三鲜。初始化就相当于买菜、洗菜、切菜、炒菜的过程。回到代码上面,它主要做了下面两件事:

  • 给每一个存储属性赋初始值
  • 执行其他必须的设置

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PotatoPepperEggplant {
let potato: String
let pepper: String
let eggplant: String
init(potato: String, pepper: String, eggplant: String) {
//给每一个存储属性赋初始值
self.potato = potato
self.pepper = pepper
self.eggplant = eggplant
//执行其他必须的设置
cook()
}
func cook() {
//do something
}
}
let ppe = PotatoPepperEggplant(potato: "🥔", pepper: "🌶", eggplant: "🍆")
print("now you can eat\(ppe)")

除了上面的方式,我们还可以通过设置默认值的方式来给存储属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class PotatoPepperEggplant {
let potato = "🥔"
let pepper = "🌶"
let eggplant = "🍆"
init() {
cook()
}
func cook() {}
}
let ppe = PotatoPepperEggplant()

现在,我们知道了初始化就是执行构造器的过程,下面我们来看一下默认构造器和创建自定义构造器的几种方式。

默认构造器

对于值类型和引用类型,默认构造器是不同的。如果 class 给所有的存储属性赋了默认值,且没有实现任何自定义的构造器,那么 Swift 会提供一个默认的构造器。

class

1
2
3
4
5
6
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()

而对于 struct ,只要没有实现任何自定义构造器,不管它有没有给存储属性赋默认值, Swift 都会提供默认构造器。

struct

1
2
3
4
5
struct Size {
//var width, height: Double 也会提供默认构造器
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

当你给存储属性分配默认值或者通过构造器设置初始值的时候,属性的值被直接设置,不会触发属性观察

创建自定义构造器的几种方式

形参(parameter)构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius 是 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius 是 0.0

形参(parameter)和实参(argument)构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Color {
let red, green, blue: Double
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
}
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

如果声明了实参名称,调用的时候不能省略实参名称。

1
2
let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required

如果你在定义构造器时没有提供实参标签,Swift 会为构造器的每个形参自动生成一个实参标签。

不带实参的形参构造器

如果你不希望构造器的某个形参使用实参标签,可以使用下划线(_)来代替显式的实参标签来重写默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
init(_ celsius: Double) {
temperatureInCelsius = celsius
}
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0

有可选类型属性的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

有常量属性的构造器

常量属性,只能在构造器中被赋值,且一旦赋值就不可修改。子类中也不能修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// 打印“How about beets?”
beetsQuestion.response = "I also like beets. (But not with cheese.)"

到这里,我们了解了默认构造器和创建自定义构造器的几种方式,接下来我们看一下如果使用构造器代理( Initializer Delegation )来避免多个构造器的代码重复。

因为值类型是不能继承的,所以构造器代理又分为值类型的构造器代理和类的构造器代理,我们先看一下比较简单的值类型的构造器代理。

值类型的构造器代理

对于值类型,你可以使用 self.init 在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用 self.init。还有就是:如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器。这主要是为了避免在一个构造器中做了一些重要设置,但有人不小心使用自动生成的构造器而导致错误的情况。

如果你想让默认构造器、自定义的构造器都可以使用的话,你可以将自定义的构造器放在Extension中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}

通过上面的代码你可以使用init()init(origin: Point, size: Size)init(center: Point, size: Size)三种方式来初始化 Rect 的实例。

看完上面的代码你也许会有疑问:init(origin: Point, size: Size)和默认构造器是一样的,那为什么我们还要再写一遍?那是因为我们自定义了init(center: Point, size: Size)构造器,所以默认构造器已经失效,我们只能再自己写一遍。

如果你不想自己写一遍默认构造器的话,可以用下面这种方式实现上面等效的代码:

1
2
3
4
5
6
7
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}

类的构造器代理

为了确保实例中的所有存储属性都能有初始值, Swift 提供了两种构造器,分别是:指定构造器、便利构造器。

指定构造器( Designated Initializers )

定义一个指定构造器:

1
2
3
init(parameters) {
statements
}

便利构造器( Convenience Initializers )

定义一个便利构造器:

1
2
3
convenience init(parameters) {
statements
}

为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用需要遵循类类型的构造器代理规则。

类类型的构造器代理规则

规则有三条,分别是:

  • 指定构造器必须调用其直接父类的的指定构造器。
  • 便利构造器必须调用同类中定义的其它构造器。
  • 便利构造器最后必须调用指定构造器。

总结一下就是:指定构造器必须总是向上代理(去父类);便利构造器必须总是横向代理(在本类)。如下图所示:

构造器的两个阶段

Swift 中类的构造过程包含两个阶段。第一个阶段:给类中的每个存储属性赋初始值。只要每个存储属性初始值被赋值,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储属性。

Swift 通过4步安全检查来确定构造器两个阶段的成功执行:

  • 安全检查1:指定构造器必须在完成本类所有存储属性赋值之后,才能向上代理到父类的构造器。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Animal {
    var head = 1
    }
    class Dog: Animal {
    var foot: Int
    override init() {
    super.init()
    foot = 4
    }
    }

上面的super.init()会报错,因为此时 Dog 的 foot 还没有被赋值。将 init() 改为下面即可:

1
2
3
4
5
override init() {
foot = 4
//这句也可以省略,它默认是隐式调用的。
super.init()
}

  • 安全检查2:指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。

    1
    2
    3
    4
    5
    6
    //这时,你必须显式的调用super.init(),因为你要修改继承属性- head 的值
    override init() {
    foot = 4
    super.init()
    head = 2
    }
  • 安全检查3:便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。

    1
    2
    3
    4
    5
    6
    7
    convenience init(foot: Int) {
    //先调用其他构造器,如果此处不调用会编译出错
    self.init()
    //再为任意属性(包括所有同类中定义的)赋新值
    self.foot = foot
    head = 3
    }
  • 安全检查4:构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Dog: Animal {
    var foot: Int
    override init() {
    foot = 4
    super.init()
    head = 2
    // 如果上面的未完成,是不能调用run()的,因为self还没有完整的创建
    run()
    }
    func run() {
    //do something
    }
    }

现在看一下阶段一和阶段二的完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
阶段 1 - 自下而上
* 类的某个指定构造器或便利构造器被调用。
* 完成类的新实例内存的分配,但此时内存还没有被初始化。
* 指定构造器确保其所在类引入的所有存储型属性都已赋初值。
存储型属性所属的内存完成初始化。
* 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
* 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
* 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,
这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2 - 自上而下
* 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。
构造器此时可以访问 self、修改它的属性并调用实例方法等等。
* 最终,继承链中任意的便利构造器有机会自定义实例和使用 self。

第一阶段示例图 - 自下而上:

第二阶段示例图 - 自上而下:

构造器的继承与重写

  • 继承
    默认情况下子类是不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被子类自动继承。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    规则 1
    如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
    规则 2
    如果子类提供了所有父类指定构造器的实现——无论是通过规则 1
    继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。
    class Animal {
    let head = 1
    var name = ""
    init(name: String) {
    self.name = name
    }
    convenience init() {
    self.init(name: "animal")
    }
    }
    class Dog: Animal {
    let foot = 4
    }
    //自动继承父类所有的指定构造
    let d1 = Dog(name: "dog") // d1.name dog
    //自动继承父类所有的便利构造器
    let d2 = Dog() // d2.name animal
  • 重写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Vehicle {
    var numberOfWheels = 0
    var description: String {
    return "\(numberOfWheels) wheel(s)"
    }
    }
    class Bicycle: Vehicle {
    override init() {
    super.init()
    numberOfWheels = 2
    }
    }

可失败构造器

在给构造器传入无效的形参,或缺少某种所需的外部资源,又或是不满足某种必要的条件等情况下,我们创建一个可失败的构造器是非常有必要的,来看一下可失败构造器的语法:

1
2
3
4
5
6
7
8
9
10
11
12
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty {
return nil
}
self.species = species
}
// 可失败构造器不能与其他非可失败构造器(指定构造器、便利构造器)的参数和类型相同
//所以下面这个指定构造器是非法的。
//init(species: String) { }
}

值类型的可失败构造器可以横向代理到自身其他的可失败构造器;类的可失败构造器既可横向代理自身的可失败构造器,亦可向上代理到父类的可失败构造器。

但无论是向上还是横向,只要可失败构造器触发构造失败,整个构造过程将即刻停止,不会再执行后面的构造代码。

必要构造器

我们可以通过required关键字来实现必要构造器,子类必须实现父类的必要构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
var name: String
required init(name: String) {
self.name = name
}
}
class Dog: Animal {
var foot: Int
//在重写父类必要构造器的时候不需要加override
required init(name: String) {
foot = 4
super.init(name: name)
}
}
Dog(name: "dog")

有一点需要注意的就是:如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
var name: String
required init(name: String) {
self.name = name
}
}
class Dog: Animal {
var foot = 2
}
Dog(name: "dog")

在我们日常开发中,我们会经常自定义UITableViewCell的子类来实现我们定制化的需求,如果我们没有实现required init?(coder aDecoder: NSCoder)方法的话,我们的代码是编译报错的。查看文档我们发现该方法为NSCoding的方法,且该方法为UIView 必要构造器,所以它的子类必须实现该方法。

1
2
3
4
5
6
7
8
9
10
class CustomTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

练手小栗子

光说不练假把式,通过下面的几个小栗子来加深一下对构造器的理解。

分析一下下面的例子是否正确并思考错误的原因。

  • 例1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Animal {
    let head = 1
    }
    class Dog: Animal {
    override init() {
    head = 3
    }
    }
  • 例2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Student {
    var name = ""
    var age = 0
    init(stuName: String, stuAge: Int) {
    name = stuName
    age = stuAge
    }
    }
    Student(name: "jack", age: 18)
  • 例3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Animal {
    let head = 1
    }
    class Dog: Animal {
    let foot: Int
    init(foot: Int) {
    self.foot = foot
    run()
    }
    func run() {
    //do something
    }
    }
  • 例4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Animal {
    let head = 1
    }
    class Dog: Animal {
    var foot: Int
    init(foot: Int) {
    super.init()
    self.foot = foot
    run()
    }
    func run() {
    //do something
    }
    }

总结

通过上面的介绍,我们了解了默认构造器、自定义构造器类型、构造器代理、构造器的两个阶段、可失败构造器和必要构造器是什么。现在我们总结一下需要重点理解的构造器代理和构造器的两个阶段:

  • 构造器代理

    1
    2
    1、值类型只能横向代理
    2、类可以横向代理和向上代理
  • 构造器的两个阶段

    1
    2
    3
    4
    5
    6
    7
    8
    1、先确保所有的存储属性都被赋予初始值
    2、在实例准备使用之前,可以自定义存储属性的值
    通过4步安全检查来确保两个阶段成功:
    * 安全检查1:指定构造器必须在完成本类所有存储属性赋值之后,才能向上代理到父类的构造器。
    * 安全检查2:指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。
    * 安全检查3:便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。
    * 安全检查4:构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 `self` 作为一个值。

除了上面的两个重点,我们还需要注意一下几个小点:

1
2
3
4
5
* Swift 中的构造器与 Objective-C 中的不同,它并不返回值。
* 如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器
(如果是结构体,还将无法访问逐一成员构造器)。
但可以将自定义构造器写在Extension中,来避免这个问题。
* 子类可以在构造过程修改继承来的变量属性,但是不能修改继承来的常量属性。

关于构造器,自己总结的思维导图:

好了,本文到此就结束了。希望通过这篇文章能让大家对 Swift 的构造过程能有一个更清晰的认识。Enjoy Every Day!🙂