Swift之路 —— 构造过程

Author Avatar
xiaoLit Created: Jul 18, 2019 Updated: Sep 09, 2019

前言:
学习后整理,大多记录Swift区别于Objective-C的特性,来源官方文档。

一、值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能避免多个构造器间的代码重复。

构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类。这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。

二、类的继承和构造过程

类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
Swift为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们被称为指定构造器和便利构造器。

1. 指定构造器和便利构造器

  • 指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。
  • 便利构造器(convenience)是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

类类型的构造器代理

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

三、两段式构造过程

Swift中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。
优势:两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

1. 基于安全检查的两段式构造过程展示:

阶段 1:

  1. 类的某个指定构造器或便利构造器被调用。
  2. 完成类的新实例内存的分配,但此时内存还没有被初始化。
  3. 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
  4. 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
  5. 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
  6. 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。

阶段 2:

  1. 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  2. 最终,继承链中任意的便利构造器有机会自定义实例和使用 self。

注意:
Swift的两段式构造过程跟Objective-C中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值 0 或空值(比如说 0 或 nil)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以 0 或 nil 作为合法默认值的情况。

2. Swift编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成

安全检查 1:
指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。(如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。)

安全检查 2:
指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。

安全检查 3:
便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做,便利构造器赋予的新值将被该类的指定构造器所覆盖。

安全检查 4:
构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值(在实例变量初始化后即便处于第一阶段但却可以取值),不能引用 self 作为一个值。(类的实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,类的实例才是有效的,才能访问属性和调用方法。)

3. 异常

基于以上检查实现的过程时却发现了异常!

class tree {
    var num: Int?
    var name: String
    init(name: String) {
        self.name = name
    }
}

class appleTree: tree {
    var have: Bool
    init(name: String, num: Int, have: Bool) {
        self.have = have
        let testStr = "test\(self.have)"
        print(testStr)
        super.init(name: name)
    }
    override init(name: String) {
        have = true
        super.init(name: name)
    }
}

let temp = appleTree(name: "haha", num: 2, have: true)
let temp2 = appleTree(name: "haha")

Swift5.0

这里可以看到下面这段代码在上述代码中的位置

self.have = have
let testStr = "test\(self.have)"
print(testStr)

是在父类初始化代码之前(super.init),处于构造方法第一阶段中。与官方文档说“构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值”不符合。该图相当于一个实例变量被初始化了,就能立即使用了,哪怕这个实例还没有完全被初始化。有些不解,遂去查看了官方英文版发现语义完全相同。经多方求证后,得到了SwiftGG组大佬的评价。

但是联系苹果官方实在繁琐,索性先摆着了,待有时间再说。

四、构造器的继承和重写

Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上 override 修饰符。

相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文 类的构造器代理规则 有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加 override 修饰符。

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}
let vehicle = Vehicle()
 print("Vehicle: \(vehicle.description)")
 // Vehicle: 0 wheel(s)

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}
 let bicycle = Bicycle()
 print("Bicycle: \(bicycle.description)")
 // 打印“Bicycle: 2 wheel(s)”

如果子类的构造器没有在阶段二过程中做自定义操作,并且父类有一个无参数的指定构造器,你可以在所有子类的存储属性赋值之后省略 super.init() 的调用。

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() 在这里被隐式调用
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}
let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
 // Hoverboard: 0 wheel(s) in a beautiful silver

1. 构造器的自动继承

如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:

规则 1:
如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。

规则 2:
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。

即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。

注意:
子类可以将父类的指定构造器实现为便利构造器来满足规则 2。

五、可失败构造器

有时,定义一个构造器可失败的类,结构体或者枚举是很有用的。这里所指的“失败” 指的是,如给构造器传入无效的形参,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。

为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在 init 关键字后面添加问号(init?)。

注意:

  • 可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
  • 严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 return nil 表明可失败构造器构造失败,而不要用关键字 return 来表明构造成功。

1. 构造失败的传递

类、结构体、枚举的可失败构造器可以横向代理到它们自己其他的可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

注意:
可失败构造器也可以代理到其它的不可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。

2. 重写一个可失败构造器

如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

注意:
你可以用非可失败构造器重写可失败构造器,但反过来却不行。

3. init! 可失败构造器

通常来说我们通过在 init 关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在 init 后面添加感叹号的方式来定义一个可失败构造器(init!),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。

你可以在 init? 中代理到 init!,反之亦然。你也可以用 init? 重写 init!,反之亦然。你还可以用 init 代理到 init!,不过,一旦 init! 构造失败,则会触发一个断言。

4. 异常


Swift5.0

可以看到doc2在使用的时候仍然需要解包,所以这个隐式解包并没有产生作用,不知道其隐式解包的使用场景是否有其他限制。

类比对于常量/变量的?!是很明确的隐式解包。

Swift5.0

六、必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器:

class SomeClass {
    required init() {
        // 构造器的实现代码
    }
}
class SomeSubclass: SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

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

七、通过闭包或函数设置属性的默认值

如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

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

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

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

注意:
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。