Swift构造过程

独奏

技术分享|2015-7-25|最后更新: 2023-2-23|
type
status
date
slug
summary
tags
category
icon
password
Swift 的构造过程算是比较复杂的,《The Swift Programming Language》 中关于 Initialization 的信息量也是非常大。
Swift 中构造器的写法如下
与 Objective-C 不同, - Swift 的构造器是没有返回值的 - Swift 的构造器是通过参数列表来区分的 - Swift 的构造器默认情况下是不被继承的

## 存储型属性的初始赋值

Swift 中分『存储型属性』(stored properties)和『计算型属性』(computed properties)。
类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态。 > Classes and structures must set all of their stored properties to an appropriate initial value by the time an instance of that class or structure is created. Stored properties cannot be left in an indeterminate state.
这里有两点说明
  • 枚举类型本身只能包含计算型属性,所以枚举类型是除外的
  • 当存储类型是 可选类型(optional) 时,Swift 会自动帮你赋初始值 nil。也就是说,可选类型的存储型属性可以既不在定义中为其赋值,也不在 init 中将其初始化。
设置初始值的方式
  • 在定义时提供默认值
  • 在构造器(init)中初始化

## 定制构造过程

你可以在定义构造器时提供构造参数,为其提供定制化构造所需值的类型和名字。构造器参数的功能和语法跟函数和方法参数相同。
构造器跟函数和方法不同的是 构造器要求每一个参数都有外部名字(external parameter name),如果没有显示的指定外部名字,那么编译器会自动生成一个与内部名字相同的外部名字。
如果你不希望为构造器的某个参数提供外部名字,可以使用下划线_来描述外部名来覆盖默认行为

## 常量初始化

在构造器中可以对常量进行初始化操作
注意: 对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改,不能在子类中修改。

## 默认构造器

Swift 将为所有属性已提供默认值的且自身没有定义任何构造器的结构体或基类,提供一个默认的构造器。这个默认构造器将简单的创建一个所有属性值都设置为默认值的实例。 > Swift provides a default initializer for any structure or base class that provides default values for all of its properties and does not provide at least one initializer itself. The default initializer simply creates a new instance with all of its properties set to their default values.
注意默认构造器的条件
  • 所有存储型属性都有默认值(可选类型的属性可以不指定默认值)
  • 没有显示创建任何构造器
  • 本身是结构体或者基类
可以预见,默认构造器的形式为 init(){}

结构体逐一成员构造器

逐一成员构造器 是结构体独有的。我们在调用 逐一成员构造器 时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

## 值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。
构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理任务给本身提供的其它构造器(即self.init(…));类则不同,它可以继承自其它类,这意味着 类有责任保证其所有继承的存储型属性在构造时也能正确的初始化
值类型的构造器这里相对简单,就不细说了

## 类的继承和构造过程

类里面的所有存储型属性(包括所有继承自父类的属性)都必须在构造过程中设置初始值。
Swift 提供了两种类型的类构造器来确保所有类实例中存储型属性都能获得初始值,它们分别是指定构造器(designated initializers)和便利构造器(convenience initializers)。
convenience 修饰的是便利构造器,其他的是指定构造器。每个类都必须拥有至少一个指定构造器,这个指定构造器可以是从父类继承来的。

构造器链

为了简化「指定构造器」和「便利构造器」之间的调用关系,Swift采用以下三条规则来限制构造器之间的代理调用:
规则一,指定构造器必须调用其直接父类的指定构造器; 规则二,便利构造器必须调用同一类中定义的其他构造器; 规则三,便利构造器必须最终调用一个指定构造器;
简言之就是指定构造器必须向上调用,便利构造器必须横向调用
notion image

两段式构造

Swift中类的构造过程包含两个阶段。
  • 第一阶段,每个存储型属性通过引入它们的类的构造器来设置初始值。
  • 第二阶段,在新实例准备使用之前进一步定制存储型属性。
安全检查
  1. 指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其他构造任务向上代理父类的构造器;简而言之,子类的构造器在 super.init(...) 之前必须确保自己的存储型属性(非继承属性)都完成了初始化。
  1. 指定构造器必须在调用父类构造器之后再为继承的属性设置新值。否则,指定构造器赋予的新值将被父类中的构造器所覆盖。
  1. 便利构造器必须在调用同一类中的其它构造器之后再为其它属性赋值。否则,便利构造器赋予的新值将被同类中其它指定构造器所覆盖。
  1. 构造器在第一阶段构造完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self的值。类实例在第一阶段结束以前并不是完全有效,仅能访问属性和调用方法,一旦完成第一阶段,该实例才会声明为有效实例。
两段式构造详细说明
阶段一
  • 某个指定构造器或便利构造器被调用;
  • 完成新实例内存的分配,但此时内存还没有被初始化;
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化;
  • 指定构造器将调用父类的构造器,完成父类属性的初始化;
  • 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部;
  • 当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段1完成。
阶段二
  • 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。
  • 最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。
下图展示了在假定的子类和父类之间构造的「阶段一」:
notion image
以下展示了相同构造过程的「阶段二」:
notion image

类构造器的继承和重载

跟OC中的子类不同,Swift 中的子类不会默认继承父类的构造器。
当你要重载一个父类的指定构造器的时候,不论是重载为指定构造器还是便利构造器,必须加上 override 关键字。 > You always write the override modifier when overriding a superclass designated initializer, even if your subclass’s implementation of the initializer is a convenience initializer.
构造器自动继承
特定条件下,父类构造器是可以被自动继承的,条件如下:
  • 如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器
  • 如果子类提供了所有父类指定构造器的实现(不管是继承过来的还是重载的),他将自动继承父类的便利构造器
这里注意
  • 如果子类定义了指定构造器(不论是不是重载),那么就意味着子类不会继承父类所有指定构造器了。这种情况下,除非子类重载了父类所有指定构造器,否则不会继承父类的便利构造器。
  • 如果存在存储型属性没有指定默认值(可选类型默认为 nil),那么编译器会强制你提供指定构造器来初始化存储型属性

## 可失败构造器(Failable Initializer)

可失败的构造器就是一个允许返回 nil 的构造器。在构造器中我们不需要显式的return self因为编译器为我们做了这个工作,但是对于可失败的构造器来说,就需要我们显示的retun nil,我们只要在必要的地方retun nil就好了,return self编译器依然会自动添加。
说明
  • 可失败的构造器并不是类独有的,结构体和枚举也同样适用
  • 由于构造器是通过参数列表来区分的,所以一个构造器声明为可失败构造器之后,就不能再声明为非可失败构造器了,比如不能同时出现init?(){}init(){}
  • 值类型(如结构体和枚举类型)可以在任意时刻触发失败行为。但是类的可失败构造器只能在所有类属性被初始化完毕和调用完父类的构造器之后才能触发失败行为。
  • 可失败构造器可以调用同级或上级的可失败构造器
  • 非可失败构造器不能调用可失败构造器
  • 可以用一个非可失败构造器覆盖一个可失败构造器,但反过来就不行了。

可失败构造器 init!

可失败构造器除了 init? 之外还有另一种形式 init!init!是自带解包(implicitly unwrapped)的.

## 必要构造器(Required Initializers)

如果在类的构造器前加required关键字,则要求该类的所有子类都必须实现该构造器,且实现的时候也必须加required关键字,也就是说必要构造器是一直向下传递的。
这里最常用的就是当我们某一个类继承UIViewController然后定义了指定构造器之后,编译器会要求必须实现init(coder aDecoder: NSCoder)构造器。这个init(coder aDecoder: NSCoder)构造器是在NSCoding协议中定义的。 也就是说当我们没有定义任何指定构造器的时候,必要构造器是可以继承下来的,一旦我们定义了指定构造器,那么我们就必须实现必要构造器

## 结语

《The Swift Programming Language》中对于构造器的描述是一个大长篇,这里虽然做了一些精简,但是依然是一个长篇,里面的内容值得我们仔细琢磨,在实践中多多体会。
这里我依然有个问题还不是太明白:
在 UIViewController 中定义了指定构造器init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) 但是却可以通过UIViewController()来调用默认构造器,这个不清楚是怎么做到的。
答案:UIViewController 的这个无参的构造器是从 UIKit 里面迁移过来的,也就是说这个无参的构造器实际上就是 Objective-C 的 UIViewController 的无参构造方法。 所以当我们继承 UIViewController 之后,在自己的必要构造器里面是不能调用这个无参的构造器的,编译器会报错 同理 UIView 也有类似的构造器 由于目前 Cocoa 框架并没有用 Swift 重写,我们开发中使用 Cocoa 框架虽说可以通过 Swift 调用,但毕竟是 Objective-C bridge过来的,用起来还是蛮蛋疼的。 ——2015-7-29