19 Oct 2017 by 晓晨
全文都是猜的。全文省略“我觉得”、“我猜”等词。
同 Swift 中关于属性的猜测,我只是想用自己的话表述一下相关概念,其中有自己的猜想,但是我的猜想是符合我理解的文档中的各种规则的。
Swift 为构造器指定各种规则,目的就是确保所有的属性都被正确初始化,这里所说的正确初始化可能包括下面这些要求,
要求一:所有的 stored 属性都被初始化,包括整个继承链上的父类。
要求二:避免构造器循环调用
子类构造器 A 中调用了父类构造器 A,而父类构造器 A 又调用了父类的另一个构造器 B,而 B 又在子类中被覆盖,子类中的 B 又调用了子类中的 A。
要求三:避免属性在初始化前被访问,或者在设置后又被父类构造器意外重写,当前类中的构造器的重写是在可控的预期之中的。
Swift 制定了各种规则,如下,
指明了当前类中所有构造器之间的调用依赖,至少指明了最终被调用的构造器,确保任何一个构造器最终都只会调用一次父类的构造器。从一个方面减少属性被构造器意外重写的可能。
对父类构造器的调用只能通过当前类的 designated 构造器,确保如果可能发生构造器循环,必须途径 designated 构造器这条主干;同时,designated 构造器不能调用本类的其它构造器,阻止了构造器调用从 designated 构造器这条主干向其它构造器的发散。根本上避免了构造器循环调用。
这些规则都是由上述规则引申的,遵循下面的规则是被迫的,只是为了遵守上面的规则。
Convenience 构造器是当前类独有,除特殊情况下,不能被继承。
因为 convenience 构造器只能调用当前类的 designated 构造器,如果允许 convenience 构造器被继承,那么子类在调用该构造器时将会绕过自己的的designated 构造器,违反了规则二,convenience 构造器必须调用当前类中的另一个构造器。
Convenience 构造器不能被覆盖(如果不能在实现中调用父类方法就不叫覆盖的话)
其实按照覆盖的定义,
A subclass can provide its own custom implementation of an instance method, type method, instance property, type property, or subscript that it would otherwise inherit from a superclass. This is known as overriding.
因为不能被继承,所以当然不能被覆盖。
但严格意义上,convenience 构造器在某些特殊情况下是可以被继承的,这个特殊情况是 automatic initializer inheritance,后面再详细说。
所以,我们不能用覆盖的定义来否决 convenience 构造器被覆盖的权力,实际上它不能被覆盖的原因按照文档中的说法如下,
Conversely, if you write a subclass initializer that matches a superclass convenience initializer, that superclass convenience initializer can never be called directly by your subclass, as per the rules described above in Initializer Delegation for Class Types. Therefore, your subclass is not (strictly speaking) providing an override of the superclass initializer. As a result, you do not write the override modifier when providing a matching implementation of a superclass convenience initializer.
意思是,当在子类中写和父类 convenience 构造器同名的构造器时,其内部实现不能直接调用到父类的这个构造器,所以说子类并没有提供覆盖,并不能叫做覆盖。
文档中就是这样说的,没法反驳。
但我认为,按照上面引用的覆盖的定义,在子类中写与从父类继承到的某个方法同名的方法,就叫覆盖。
当发生 automatic initializer inheritance 时,在子类中写继承到的 convenience 构造器就是覆盖。
当然了,因为文档认为不是覆盖,所以在任何情况下,只要是和父类的某个 convenience 构造器重名,就不能写 override 关键字。
注意,上面这句话,时间一久,脑子里记得可能就走样了,看到一个构造器被标记上了 convenience 和 override,就觉得有错误。实际上,convenience 和 override 两个关键字可以共存,只要被覆盖的不是 convenience 构造器就行了(那就只能是 designated 构造器了)。
回到 convenience 构造器不能被覆盖这条规则,其原因子类 convenience 构造器中不能调用父类构造器的这条规则就是 rules for delegation calls between initializers 中制定的,所以才说,convenience 构造器不能被覆盖是其引申规则。
第一阶段,初始化所有的 stored 属性,包括父类的,不管是不是想要的结果,在该阶段完成时,所有的 stored 属性都有初始值了。
此时,可以通过 self 访问属性和方法了,相当于这个实例已经可以用了。
第二阶段,做的事情和普通实例方法没区别,只是把整个实例调整到一个可以用的样子。
(调用当前类的 convenience init -> )调用当前类的 designated init -> 初始化当前类中的属性 -> 调用父类的 designated init -> 初始化父类中的属性 -> 调用父类的父类的 designated init -> 初始化父类的父类的所有属性
-> | 两阶段分割点 | -> |
父类的父类的 designated init 中初始化所有属性之后的部分 -> 父类的 designated init 中初始化所有属性之后的部分 -> 当前类中 designated init 中初始化所有属性之后的部分( -> 当前类 convenience init 中调用 designated init 之后的部分)
保证二阶段初始化的是四条安全检查(safety checks),它们最终保证了要求一和要求三。
上面的描述有一点不完善的地方,注意下面两条规则中的区别,
A designated initializer must delegate up to a superclass initializer before assigning a value to an inherited property.
A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class).
这个区别的结果就是,在 designated 构造器中,在 delegate up 父类 designated 构造器前,在当前类引入的所有属性初始化之后,可以访问当前类的属性(不能是继承属性,因为还没初始化)。
class C {}
class D: C {
let i = 0
override init() {
print(i)
super.init()
}
}
也就是说,上面对两阶段初始化的描述中,分割点之前也可能出现一部分对当前属性的访问,但这并没有违背要求三,因为,此时访问的属性已经初始化了,所以没有在初始化前访问;父类不会修改当前类的属性,所以不用担心父类的重写。
通常情况下,我们没有必要把对当前类引入的属性的修改放在其初始化之后、调用父类构造器之前,我想到一个需要这样做的场景,如下,
class C {
init?() { /* expensive things */ }
}
class D: C {
let i = 0
override init?() {
if i == 0 {
return nil
}
super.init()
}
}
如果子类构造器是 failable 的,在调用父类构造器前就及时中止能够减少调用父类构造器的开销。不过我觉得除非必要,就别放这么奇怪的位置。
Swift 默认是不继承构造器的,为了防止继承的构造器不能初始化子类引入的属性。但当符合一定要求时,会有自动构造器继承这个机制。这个机制本身是为了省事的,它所遵循的规则保证了构造器继承是安全的。
自动构造器继承的大前提是当前类引入的新属性都有提供默认值。
第一条规则是关于 designated 构造器的自动继承,只要子类中定义了 designated 构造器,就不会有自动构造器继承。不管子类中的这个 designated 构造器是直接写的、或是覆盖的父类的构造器(说覆盖只能说覆盖的是父类的 designated 构造器,convenience 构造器不能覆盖)、或是写与父类中 convenience 构造器同名的 designated 构造器(这不叫覆盖)。
第二条规则是关于 convenience 构造器的自动继承,是独立于第一条的,只要父类的所有 designated 构造器在子类中都有(不管是自动继承或是覆盖),就自动继承父类的所有 convenience 构造器。
大前提确保所有属性都被初始化。
第一条规则确保我想在 designated 构造器中做的不会被继承的 designated 构造器跳过。
第二条规则确保我想在 designated 构造器中做的不会被继承的 convenience 构造器跳过。
第一条规则符合 Swift 在很多情况下的做法,要么自动帮你做符合你预期的事情,但只要你决定自己来,那就完全交给你做,如果你不写 designated 构造器,那我就帮你继承下来父类的所有 designated 构造器,但只要你自己写了,决定自己来,那我就完全不管了,一个 designated 也不帮你继承。
第二条规则保证 convenience 构造器总是会调用当前类的 designated 构造器,从而符合构造器相互之间调用的规则。
当前类引入的属性尽量在定义时给好初始值
需要执行语句才能初始化的属性可以写在 closure 中并且立即调用
如果只是想对一些属性做额外的调整,应该使用 convenience 构造器,convenience 可以覆盖父类的 designated,只要调用了当前类的 designated 就行(可以是继承得来的)。如果不确定是否应该写 convenience,可以先照着 convenience 去写,如果其中无法调用本类的或者继承得到的 designated,则可能需要写 designated 了。
如果写不成 convenience,或者有需要做一些特殊的初始化,比如 failable,或者将多个属性的初始化放在一起有助于理解(如果基于某些共同的条件判断,将属性初始化分布在多个 closures 中会很乱),再考虑写 designated 构造器。
一旦写了 designated 构造器,就应该尽量覆盖父类所有的 designated 构造器,从而自动继承其所有 convenience 构造器,这里的覆盖可以用 convenience 构造器覆盖。