“理解”Swift 的构造函数

2,244 阅读8分钟
原文链接: www.jianshu.com

参考:《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》 书中的第16条:提供“全能初始化方法”。
虽然里面写的是OC中的初始化方法的编写规则和注意事项,但是从这里面可以理解为什么swift要知道指定构造器和便利构造器,为什么要维护指定构造器的调用链,构造器的继承为什么有那么多规则等。其实swift就是强制性的让我们执行了这些编写规则和注意事项。

默认构造器

类没有默认的逐一初始化器。
如果类的所有的属性都有默认值,Swift才会为它们提供默认构造器

init()

当使用ClassName()来创建对象时,默认调用的就是没带参数的init()方法。
当类有属性没有初始化时,必须要指定构造器来初始化。

指定构造器和便利构造器

如果创建一个类的方式不止一种,那么这个类就有多个构造器,这当然很好。不过仍然要在其中间选定一个构造器作为指定构造器,令其他构造器(便利构造器)来调用它。
NSDate就是一个例子:

open class NSDate : NSObject, NSCopying, NSSecureCoding {
    public init()
    public init(timeIntervalSinceReferenceDate ti: TimeInterval)
    public init?(coder aDecoder: NSCoder)
}
extension NSDate {
    ...
    public convenience init(timeIntervalSinceNow secs: TimeInterval)
    public convenience init(timeIntervalSince1970 secs: TimeInterval)
    public convenience init(timeInterval secsToBeAdded: TimeInterval, since date: Date)
}

正如该类的文档所述的那样,在上面几个初始化方法中,public init(timeIntervalSinceReferenceDate ti: TimeInterval)是指定构造器,也就是说,其余的便利构造器都要调用它,于是指定构造器中才会存储内部数据,这样的话,当底层数据存储机制改变时,只要修改此构造器的代码就好,无需改动其它便利构造器。

指定构造器内部结构:

init(...) {
  //step1: 初始化自己的属性
  ...
  //step2: 调用父类的指定构造器
  ...
  //step3: 做想做的事,比如修改父类属性,调用对象方法等。
  ...
}

便利构造器内部结构

convenience init(...) {
  //step1: 调用同一类的指定构造器
  ...
  //step2: 做想做的事,比如修改属性值,调用对象方法等。
  ...
}

指定构造器与便利构造器之间的调用关系:

  • 在指定构造器中所有的属性都要被初始化
  • 指定构造器必须调用其直接父类的的指定构造器
  • 在属性还没被全部初始化前,不能使用调用任何对象方法,访问或修改属性值。
  • 便利构造器在调用了同一类指定构造器后才能访问和修改属性值

两段式构造函数:

  • 第一个阶段: 每个存储属性都获得一个初始值
  • 第二个阶段: 进一步定制存储属性的值

例子:
定义一个表示矩形的类:

class EOCRectangle {
    var width: Float
    var height: Float

    //自定义指定构造器
    init(withWidth width: Float, height: Float) {
        //初始化所有属性
        self.width = width
        self.height = height
    }
}

我们希望必须提供width和height来创建矩形,当我们自定义了指定构造器init(withWidth width: Float, height: Float)后,就无法使用init()这个构造器来初始化了。
如果我们希望可以通过EOCRectangle()来创建对象,这时给出默认的width和height,可以用以下两种方式实现:
方式1:

//将init()变为便利构造器,调用同一类的指定构造器
convenience init() {
  self.init(withWidth: 10, height: 20)
 }

方式2:

//直接在init里面为存储属性赋值
init() {
  self.width = 10
  self.height = 20
}

推荐使用第一种,因为当底层数据存储机制改变后,只需修改init(withWidth width: Float, height: Float)即可,无需改动init(),比如将width和height改成存储在一个结构体里面。

下面定义一个正方形类EOCSquare为EOCRectangle的子类:

class EOCSquare: EOCRectangle {
    init(withDimension dimension: Float) {
        //调用父类的指定构造器
        super.init(withWidth: dimension, height: dimension)
    }
}

自定义了指定构造函数,指定高度宽度必须相等才能初始化。此时外部只能通过EOCSquare(withDimension: 10)这种方式来创建EOCSquare对象。父类的指定构造器init(withWidth width: Float, height: Float)无法使用
若我们希望init(withWidth width: Float, height: Float)依然可用,但是是取width和height中的最大值来作为边长:
写法1:

override init(withWidth width: Float, height: Float) {
        let dimension = max(width, height)
        super.init(withWidth: dimension, height: dimension)
    }

写法2:

convenience override init(withWidth width: Float, height: Float) {
        let dimension = max(width, height)
        self.init(withDimension: dimension)
    }

这两种写法的共同点:

  • 都重写了父类的构造函数init(withWidth width: Float, height: Float)
  • 自动继承了父类的便利构造函数convenience init()

区别:

  • 第一种方式是重写成指定构造函数,第二种方式是重写为便利构造函数,外部使用上并没有区别,只是重写成指定构造函数,需要调用父类的指定构造函数,重写成便利构造函数则需要调用自己类的指定构造函数

此时EOCSquare可以通过以下三种方式初始化了:

EOCSquare() //调用父类的便利构造函数convenience init() width:20 height:20
EOCSquare(withWidth: 30, height: 15) //调用自己重写的构造函数width:30 height:30
EOCSquare(withDimension:25) //调用自定义的指定构造函数width:25 height:25

由上面的例子可以得出指定构造函数和便利构造函数在继承上的规则

  • 子类没有自定义指定构造函数时,继承父类所有的指定构造函数。
  • 子类自定义了指定构造函数,不继承父类任何的构造函数。
  • 子类实现了父类所有的指定构造函数时(包含第一条),自动继承父类所有的便利构造函数。

编写多个指定构造函数

比如说某个对象的实例有两种完全不同的创建方式,必须分开处理,那么就会出现这种情况。
NSCoding协议为例,此协议提供了序列化机制(serialization mechanism),对象可依此指明自身的编码(encode)和解码(decode)方式。UIKit就用了此机制将对象序列化,并保存至XML格式的'NIB'文件中。系统在解压缩(unarchiving)的过程中解码视图控制器。NSCoding协议定义了下面这个初始化方法,遵从该协议者都应该实现此方法:

public init?(coder aDecoder: NSCoder)

我们在实现此方法时通常都不调用平常所使用的指定构造器,因为该方法要靠解码器(aDecoder)将对象数据解压缩,所以和普通的初始化方法不同。而且如果父类也实现了NSCoding协议,那么还需要调用超类的init(coder: )方法,因此,严格的说,在这种情况下出现了两个指定构造函数。
具体到EOCRectangle这个例子上,其代码就是:

class EOCRectangle:NSObject, NSCoding {
    var width: Float
    var height: Float

    convenience override init() {
        self.init(withWidth: 10, height: 20)
    }

    init(withWidth width: Float, height: Float) {
        self.width = width
        self.height = height
    }

    required init?(coder aDecoder: NSCoder) {
        self.width = aDecoder.decodeFloat(forKey: "width") as Float
        self.height = aDecoder.decodeFloat(forKey: "height") as Float
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(width, forKey: "width")
        aCoder.encode(height, forKey: "height")
    }
}
class EOCSquare: EOCRectangle {
    init(withDimension dimension: Float) {
        super.init(withWidth: dimension, height: dimension)
    }

    override convenience init(withWidth width: Float, height: Float) {
        let dimension = max(width, height)
        self.init(withDimension: dimension)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

注意,如果EOCSquare没写required init?(coder aDecoder: NSCoder)编译会报错,因为如果子类需要添加异于父类的初始化方法时,必须先要实现父类中使用required修饰符修饰过的初始化方法,并且也要使用required修饰符而不是override。

每个子类的指定构造函数都必须要用父类相应的指定构造函数,并逐层向上。维系了指定构造函数的调用链。

总而言之,swift构造器的种种规定都是为了让我们一定是调用正确的初始化方法来初始化我们的对象,例如我们规定必须使用EOCRectangle(withWidth:20, height: 20)这种方式来创建矩形时,外部却想通过EOCRectangle()(没有在类中重写init()时)来创建,编译就会报错,因为根本就没有这个构造方法。

在OC中这些规则的实现

  • 对于类的多个初始化方法,要选定一个作为全能初始化方法,其它初始化方法都来调用它
  • 子类的全能初始化方法要调用超类的全能初始化方法
  • 如若子类的全能初始化方法与超类方法的名称不同,那么总应覆写超类的全能初始化方法,以下二选一:
    • 在重写的方法中使用子类的全能初始化方法
    • 抛异常,强制性要求使用子类的全能初始化方法来构造

这样就能维系全能初始化方法的调用链,从而保证正确的构造。