这段时间用Swift写项目时候便发现它的构造函数相比OC真的是复杂了许多,加了很多的限制和条件,不过这么设计肯定有它的道理..稳定性和可选类型的加入可能是主要原因..
通过<Swift Programming Language>和最近项目实战对我目前遇到的写构造器的各种场景和对应问题做个总结..同时用代码验证一下书中所说的各种限制...(只讨论class,struct和enum是值类型不支持继承)
Swift Programming Language 中对"构造过程"的描述
1. 类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态.
在OC中我们不需要考虑属性的初始值是因为属性默认值就为nil或者0...然而Swift中有了可选类型的概念它并不会这么做,对于可选类型的属性如果不初始化值它默认为nil..但是对于非可选类型它永远不可以为nil,我们需要手动在这个实例出现之前(构造器中或声明时候)为它这些属性赋初始值,以保证这个实例每一个属性都合法..对于计算属性因为必须实现get方法相当于也有初始值.
注: Swift 的nil和 Objective-C 中的nil并不一样。在 Objective-C 中,nil是一个指向不存在对象的指针。在 Swift 中,nil不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为nil,不只是对象类型。
OC中一个int 类型的属性没有初始值时候默认为0...但Swift的Int属性声明为可选类型(Int?)它也可能是nil,表示目前没有值.
2. 如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器
这一条很好理解...但是有一种情况是这个类有父类,并且父类有指定构造器...那么这个类会继承父类的指定构造器,相当于它具有了指定构造器.这种时候虽然没有自定义构造器,但其实Swift并不会给它默认的构造器了.
3. 类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
这与第1条描述的一件事情,因为子类会继承父类的属性,所以子类实例的属性还是要全部就有初始值.
4.1 指定构造器必须调用其直接父类的的指定构造器
一个子类实例的属性全都有初始值的前提除了子类定义的属性全部赋初值当然还要给从父类继承来的属性赋初值..调用父类的指定构造器可以保证父类的所有属性成功赋初值,进而根据父类链继续往上调用父类的构造器来完成这个子类实例所有属性的赋值.
注: 详细请看下面"类的两段式构造"
问题:为什么限定必须是父类的指定构造器而不能是一个便利构造器呢?最终便利构造器会调用到一个指定构造器,父类的属性还是会全部初始化..这样规定的原因回头再研究.
4.2 便利构造器必须调用同类中定义的其它构造器。
4.3 便利构造器必须最终导致一个指定构造器被调用。
所谓便利构造器,意思也是在属性全部初始化的基础上再给添加一些其它的赋值,或者是给可选类型通过参数赋值..所以4.3调用一个指定构造器是必需的了..4.2是说只能调用同类中的.看来便利构造器还是对自身类的初始方法,在便利构造器中调用super构造器方法会报错.
对第4条,做一个验证.
所以在给一个类添加extension的时候,通常都是使用便利构造器,先调用自身的某一个指定构造器然后再这个基础上添加某些属性...例如给一个UIButton添加快捷创建方式:
Swift的两段式构造过程
第一个阶段,每个存储型属性被引入它们的类指定一个初始值。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。
这个翻译的说实话我感觉就一个意思..就是存储型属性要首先有个初始值.因为其它语言的属性都是有默认值的,swift的属性不声明为可选类型必须要有初始值所以加了这么一个第一阶段...
第一阶段:
1. 程序调用子类的某个构造器
2. 为实例分配内存, 此时实例的内存还没有被初始化
3. 指定构造器确保子类定义的所有实例存储属性都已被赋初值
4. 指定构造器将调用父类的构造器, 完成父类定义的实例存储属性的初始化
5. 沿着调用父类构造器的构造器链一直往上执行, 直到到达构造器链的最顶部
第二阶段:
1. 沿着继承树往下, 构造器此时可以修改实例属性和访问self, 甚至可以调用实例方法
2. 最后, 构造器链中的便利构造器都有机会定制实例和使用self
Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程能不出错地完成:
直接看它所说的4种安全检查吧.
安全检查1: 指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
为什么类中所有的属性必须初始化完成才可以调用父类的构造任务呢?
一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类引入的属性在它往上代理之前先完成初始化。
这个应该不是原因..因为文档还说:一旦父类中所有属性都有了初始值,实例的内存被认为是完全初始化..所以super之后再给子类的属性赋值为什么不行呢? 答案 参考
安全检查2: 指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
事实是如果不这么做,编译器会直接报错.
安全检查3:便利构造器必须先调用同一个类的其他构造器, 然后才能对属性赋值如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。
安全检查4: 构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值
因为第一阶段完成之前内存初始化还没有成功,显然还没有self这个东西..也没有那些示例属性呢.
在项目中遇到的一个棘手问题
需求是一个逆向传值,,只不过这个闭包是在初始化构造函数里直接赋值..并且还要在闭包中能访问当前实例属性.
闭包作为构造器参数传值同其它类型作为参数时候的语法相同..因为闭包也是一种数据类型嘛
但是想要给传来的闭包直接进行赋值并且能在闭包中访问属性需要将这个带有闭包的对象使用lazy 懒加载...如果不用lazy,此类中这个属性在初始化的时候类实例并没有构造完成,使用了lazy意味着这个属性在类实例用到的时候才会初始化,所以这个时候当前类实例肯定是存在的了.才可以在闭包中使用self..
好处:很多UI控件用懒加载会缓解加载类时候的压力.
场景:属性本身依赖于外部因素才能初始化;可能会进行大量CPU消耗;不一定会在什么时候用到,一个事件触发才会用到;
特别注意,项目中我在闭包中使用self.age 编译器一直会报错...最后找了半天原因是因为我没有给这个懒加载的属性设置"类型描述" 也就是冒号后面那个..这可能是编译器的推断出现了问题,看来以后没事还是加上类型描述吧.