Swift 构造器探究
什么时候要用构造器?
对于类(Class)
其实在其他语言中,比如说Java对于属性的初始化没有严格的要求。甚至在Model层只有对应属性的get,set访问器。而在Swift中无论是对于结构体(Structure)还是类(Class),如果其中存在存储属性(stored property),那么必须在合适的地方给它赋初始值,也就是初始化。不能让它们成为不确定的状态,即没有初始化。关于初始化,Swift提供了两种方式。一种是属性定义的时候初始化,也就是赋默认值。 第二种是在构造器中初始化。这里做个小结:存储属性必须初始化,初始化的方式且只能在以上两种方式选择,并且至少包含其中一种。
而对于第一种,在Swift中又有两种初始化方式。第一种,是给予明确的值。比如说var name = "Bob"
Swift的数据类型推断机制会自动推断出name
是一个String类型变量,初始值为Bob
。第二种,是Optional类型初始化,通常用在这个属性在程序运行过程中可能存在值也可能不存在值的时候。比如说var avatar: UIImage?
一个人可能没有头像。假设avatar
是Person
类的一个存储属性,程序在运行过程,如果Person
类创建一个实例let myPerson = Person()
, 如果没有给myPerson.avatar
赋值,那么myPerson
中avatar
属性自动初始化为nil
。如果myPerson.avatar = UIImage(named: "prettyGirl")
,那么avatar属性初始化就是UIImage(named: "prettyGirl")
。
而对于第二种初始化方式,更加具体的说,是如果没有是实现第一种初始化方式的时候,必须实现的。也就是说如果你定义一个属性var avatar: UIImage
既没有给予明确的初始值,也没有让它成为Optional类型,那么必须实现在构造器中的初始化。这就是什么时候要用构造器的重点了。
然而Swift的构造器又有两种,一种是designated构造器,一种是convenience构造器。所有designated构造器都必须初始化那些没有满足第一种情况的存储属性。注意这里是类中所有的designated构造器都必须要做这件事。具体怎么做请看后文Designated构造器
对于结构体(Structure)
Swift中结构体和类的构造器其实差不多。除了结构体中没有析构器(Deinitializer),不能够继承(inherit)以及结构体有memberwise构造器外大体上是一致的。所以你有时候看到一个结构体struct Point
有两个存储属性var x: Double
, var y: Double
,却没有任何构造器,但是他们既不符合类中讨论的第一种初始化方法(即赋默认值)。那么它们违背了语法规则吗?其实不是的,如果结构体没有自定义的构造器,Swift隐式创建了一个init(x:y:)
的构造器其内容就相当于self.x = x, self.y = y
。如果你手动给结构体加个空内容的构造器init(){ }
,编译器就报Return from initializer without initializing all stored properties
的错误。也就是说,如果你创建了你自己定义的构造器,Swift就默默地帮你把memberwise构造器去掉了,而你自己定义的构造器又没有对存储属性初始化,那么这违背了语法规则。但是如果你想同时拥有这两个构造器(memberwise构造器和自定义构造器),你可以把自定义的构造写到Extension Point{ // custom initializer }
里面进去。
构造器的继承
designated构造器
designated构造器在Swift中很常见,顾名思义这个构造器就是你类中所有构造器的“原型”。在这个构造器中只调用父类的designated构造器或者不调用其他任何构造器称为designated构造器。每个类都必须至少有一个designated构造器,但是你会看到有些情况看不见类中声明designated构造器,那是因为它是一个子类,如果不写任何designated构造器,将会自动继承父类所有designated构造器。我们将在下面的自动构造器继承
中详细讲到。
init(parameters){
// statments
}
convenience构造器
convenience构造器是第二种构造器。它主要是横向代理,就是说在convenience构造器中一定存在也只能存在该类的一个构造器通常用self.init(parameters)
调用该类的一个构造器。当然convenience构造器不是必要的。
convenience init(parameters){
// 调用该类中的一个构造器
self.init(parameters)
// customize properties
}
类的构造器代理规则
- 规则1:子类中的designated构造器必须调用最近一级父类的desigated构造器
- 规则2:convenience构造器必须在同一个类中调用其他一个构造器
- 规则3:convenience构造器通过调用链(代理链)调用一个designated构造器
总结下也就说
- desingated构造器必须一直向上代理(即调用最近一级父类的designated构造器)
- convenience构造器必须横向代理,且代理终点为一个desingated构造器
下面这幅图(引用自苹果官方文档原图)就表明了这两点
SubClass的convenience构造器调用了第二个designated构造器(符合规则2),第二个designated的调用了SuperClass的designated构造器(符合规则1),这表明了convenience构造器最终调用的是designatedg构造器(符合规则3)。同理其他的构造器调用亦是如此
下面再来一幅图(引用自苹果官方文档原图)
Two-Phase初始化
第一阶段类中每个存储属性必须有初始值,一旦每个储值属性的初始状态被确定了,第二阶段就开始了。第二阶段就是在新的实例可用之前对初始值的修改阶段。利用两阶段初始化可以让初始化安全,防止属性值在初始化完成之前被访问,以及属性值被另外的构造器设置为不同的值。这和OC差不多,唯一区别就是OC在第一阶段初始的默认值只能是0或者是nil
初始化有安全检查机制
- 安全检查1:一个designated构造器在向上代理之前必须初始化该类中定义的所有存储属性
- 安全检查2:一个designated构造器必须先向上代理调用一个父类的构造器,在修改父类的属性值之前。如果不这样,那么你修改的属性值,会被父类的构造器初始化属性的时候覆盖掉。
- 安全检查3:一个convenience构造器在对本类任何属性操作之前(包括父类的属性以及本类定义的属性)必须调用另外一个本类中的构造器。如果不是这样,那么修改完的属性很可能就被本类中的构造器初始化属性的时候覆盖了。
- 安全检查4:一个构造器不能调用实例方法,读取任何实例属性的值,或者用作为一个值指向
self
知道第一阶段初始化结束
初始化两个阶段
第一阶段:
- 一个designated或者convenience构造器在类中被调用
- 类的新实例向系统申请内存空间,但是内存还没有被初始化
- 该类的一个designated构造器确认所有在该类中定义的存储属性已经被初始化。这些属性的内存被初始化。
- designated的构造器告知父类构造器对父类自己的属性执行相同的操作
- 一直代理到类继承链的最高级
- 一旦到达类继承链的最高级,并且链上的最终类确认了它所有的存储属性已经初始化完毕,则实例内存被完全初始化,然后第一阶段到此就完成了。
下面这幅图是第一阶段
第二阶段:
- 从继承链的最顶端开始每个designated的构造器可以修改实例中的属性值,此时构造器也可以访问self指针了,可以调用实例方法等等。
- 最后在继承链上的任何convenience构造器可以用self指针来修改实例
下面这幅图是第二阶段
构造器的继承和重写
Swift和绝大多数面向对象的语言不一样,它不会默认继承所有父类的构造器。也就是说你用类初始化实例的时候,只能访问到该子类构造器(除了自动构造器继承的情况)。这是因为防止用父类的构造方法来创建一个比父类多出一些属性的子类。然而如果调用父类构造方法,就不能初始化子类中的存储属性了,这样就违背了Swift的安全机制。
注意:父类中的构造器在某些特殊情况也会被自动继承,详细内容在后面的自动构造器继承会讲到
如果你在子类中写了一个构造器,并且它与父类某个designated构造器的名字相同,那么你必须用override关键字修饰init构造器。这里还得注意,就算你父类没有任何显式的构造器(但是Swift会默认给你一个init(){ // 空 }
构造器),子类在重写init() { // customize }
的时候也是要加修饰词override的,就是这样override init() { // customize }
。
注意:就算你在子类中写的是convenience类型的构造器,如果名字和父类的某个designated构造器,也必须加关键字override修饰。
还有一种情况就是如果你子类中一个构造器的名字和父类的某个convenience构造器相同,又因为父类中的convenience构造器是没办法被子类直接调用的(这个在上面类的构造器代理规则
中讲到过)。因此,严格地说,子类的convenience构造器不提供对父类convenience的重写,也就不需要用override修饰了。这个构造器就相当于新的自定义构造器,不和父类产生任何联系。
好吧讲了这么多理论估计你们都晕乎乎的,来讲点实际的例子把。
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels)wheel(s)"
}
}
这是一个基类。numberOfWheels
有了默认的初始值,也算是初始化的一种方式。description
是一个计算属性,计算属性不需要初始化。虽然看到这个类没有任何的构造器,但是前面提到过默认构造器,就是一个空的构造器init() { //空 }
下面我们来创建一个实例
let vehicle = Vehicle()
println("Vehicle: \(vehicle.decription)")
这个时候控制台打印Vehicle: 0 wheel(s)
下面我们来创建一个子类Bicycle
class Bicycle: Vehicle{
override init(){
super.init()
numberOfWheels = 2
}
}
由于Bicycle类继承Vehicle类,定义了一个init()
designated构造器,这个构造器名字和Vehicle类中的默认构造器相同,因此需要重写override
。重写中,首先调用父类的super.init()
,然后在修改父类属性的值。这符合Two-Phase初始化规则,在修改父类属性之前,先调用父类的designated的构造器,以保证父类的所有属性被初始化,并且防止修改的值被构造器的初始化过程给覆盖了。
下面我们来创建Bicycle子类的实例
let bicycle = Bicycle()
println("Bicycle: \(bicycle.description)")
控制台打印:Bicycle: 2 wheel(s)
分析:由于子类修改了父类中的numberOfWhiles
属性,因此打印结果是2
注意:子类可以在合适的位置(具体是什么位置看Two-Phase初始化中介绍)对父类的属性在初始化的时候修改,但是如果属性是常量let声明的,这是不允许的,因为let属性常量一旦被初始化赋值之后,就再也不能改了。
自动构造器继承
上面很多地方我们提到了自动构造器继承,那么这东西到底是什么呢?一般情况下,子类如果定义了自己的designated的构造器,父类的构造器是不会被继承的。但是,如果子类没有定义任何自己的构造器,那么子类就会继承父类的构造器。
假设你给你子类中任何新的属性赋予了默认值,那么一下两条规则将会被适用:
- 规则1:如果你的子类没有定义任何designated构造器,那么它会自动继承父类的所有designated构造器。
- 规则2:如果你子类提供实现了所有父类的designated的构造器(无论是通过规则1实现的,还是提供自定义手动实现的)都会继承父类所有的convenience构造器。
即使你在子类中添加了其他convenience构造器,这些规则也是适用的。
注意上面所说的提供自定义手动实现,也可以包括子类中用convenience构造器覆盖父类的designated构造器。比如说父类有个init(original:)
构造器,子类用override convenience init(original:)
也是可以使之满足规则2的
这个是有点抽象,我们来举个例子把
先定义一个基类Food
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.name(name: "[Unnamed]")
}
}
这里init(name:)
构造器是Food类的designated构造器,init()
构造器是Food类的convenience构造器,里面调用了该类的init(name:)
designated构造器
现在利用designated构造器创建一个实例
let namedMeat = Food(name: "Bacon")
现在创建了一个实例叫namedMeat
,它的name
属性值为"Bacon"
。
现在利用convenience构造器创建一个实例
let mysteryMeat = Food()
现在创建了一个实例叫mysteryMeat
,它的name
属性值为"[Unnamed]"
。
现在我们创建一个RecipeIngredient类,继承Food类
class RecipseIngredient: Foof {
var quantity: Int
init(name: String, quantity: Int){
self.quantity = quantity
super.init(name: name)
// customize superclass properties if needed
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
这里我们定义了RecipeIngredient类,它继承了Food类。其中init(name:quantity)
是它的designated构造器。init(name:)
是convenience构造器,并且是重写了父类(Food类)的init(name:)
designated构造器
构造器继承关系图
接下来我会结合Two-Phase初始化机制来讲解这个RecipeIngredient类的初始化过程。
首先明确的是,RecipeIngredient类只有一个designated构造器叫做init(name: String, quantity: Int)。当这个类初始化实例的时候后,如果使用这个构造器。首先,要满足类的代理规则1,在designated构造器中必须调用最近父类的designated的构造器。但是要先满足安全检查第一个步骤,在调用父类designated构造器之前,必须先初始化改类中定义的所有存储属性。所以对quantity属性进行初始化self.qunatity = qunatity
。初始化完成之后,调用父类(Food类)的super.init(name:)
构造器进行对父类属性的初始化,由于该例子中Food类已经是构造(代理)链的最上级了,所以在这里停止向上代理。等Food类属性初始化完成之后,返回到RecipeIngredient类中(就是执行完了super.init(name:)
),如果你想再对父类属性进行修改,那么可以在这个语句后面自定义修改。这符合安全检查机制3.
现在我们来用不同的构造器创建几个实例
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
现在实例oneMysteryItem
调用的是init()
构造器。但是我们又没有在RecipeIngredient类中看到定义,而且有了designated构造器之后Swift不会给你默认创建init()
构造器了。其实init()
来自父类Food。因为子类实现了父类所有的designated构造器(即init(name: String)
构造器),因此根据自动构造器继承的第二条规则,子类将继承父类的所有convenience构造器,也就是init()
构造器。这个init()
和内容和父类基本一致,只不过内部的self.init(name:String)调用的是RecipeIngredient类的,而不是父类的。也就是说现在oneMysteryItem
的quantity
属性值为1,name
属性值为“[Unnamed]”
。而实例oneBacon调用的是RecipeIngredient的init(name: String)
convenience构造器。所以它的name
属性的值为"Bacon"
,quantity
属性的值为1
。sixEggs实例调用的是RecipeIngredient的init(name: String, quantity: Int)
designated构造器,因此name
属性的值为"Eggs"
,quantity
属性的值为6
再来一个更加特殊的类ShopListItem
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String{
var output = "\(quantity)x \(name)"
output += purchased ? "√" : "×"
return output
}
}
你可以看到这个类没有任何构造器对purchased
的初始化,而是给予了一个默认值false
因为一件物品默认是没有购买的。
由于它没有提供任何构造器,于是符合自动构造器继承的第一条规则,它将自动继承父类RecipeIngredient的所有designated的构造器,又因为如此,也就说子类是吸纳了父类的所有desigated的构造器,自动构造器继承的第二条规则也将适用,也就是自动继承父类的所有convenience构造器。下面就是整个继承图。
现在我们创建个实例数组
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6)
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
println(item.decription)
}
控制台输出结果
1 x Orange juice √
1 x Bacon ×
6 x Eggs ×
这里第一个item是ShoppingListItem,它调用的是init()
构造器是从Food一直继承下来的,然而在RecipeIngredient层,将它初始化的内容改为了self.init(name: name, quantity: 1)
。最终到了ShoppingListItem这个类中,初始化的内容依然是self.init(name: name, quantity: 1)
这个,只不过这个self不指向RecipeIngredient,而是指向ShoppingListItem。因此初始化完毕之后第一个item的name
属性的值为“Unnamed”
,quantity
属性的值为1
,purchased
的属性的值为了false
。但是在后来breakfastList[0]把属性name
值改为了Orange juice
,把purchased
的属性的值改为了true
。所以最后的decription
是1 x Orange juice √
而其他初始化内容应该不难理解,我这里也不再重复叙述了。