由于class之间可以存在继承关系,因此它的初始化过程要比struct复杂的多,为了保证一个class中的所有属性都可以被正确的初始化,Swift引入了一系列特定的规则。在这一节中,我们就来了解class中的init方法家族。

为了演示各种init方法,我们定义了一个class Point2D,表示平面中一个点的坐标:

class Point2D {
    var x: Double
    var y: Double
}

当然,这样是不行的。上一节我们已经说过,Swift不会为class生成默认的init方法,我们必须明确定义class类型的对象的创建过程。因此,我们第一个要介绍的,就是class的默认init方法。

默认init

为了可以像这样构建一个Point2D对象:

let origin = Point2D()

我们得让Point2D有一个默认的init方法,而我们,可以通过两种方式定义默认init。第一种,就是给每一个属性都添加默认值:

class Point2D {
    var x: Double = 0
    var y: Double = 0
}

这种方式,只适合表意简单并且初始值固定的class。因为,此时我们只能创建原点位置的Point2D对象,它连一个memberwise init方法都没有。下面的代码会导致编译错误:

// Compile time error
let point11 = Point2D(x: 11, y: 11)

因此,通常,我们还是至少会为class添加一个memberwise init方法。哪怕它就是一个逐个属性赋值的方法:

class Point2D {
    var x: Double = 0
    var y: Double = 0
    
    init(x: Double, y: Double) {
        self.x = x
        self.y = y
    }
}

但这样,即便xy都有了默认值,我们之前定义orgin的代码也会导致编译错误。编译器认为我们接手了init方法的定义之后,就不会再插手init的工作。因此,如果我们定义了memberwise init,那么最好还是把属性的默认值写成memberwise init方法的默认参数:

class Point2D {
    init(x: Double = 0, y: Double = 0) {
        self.x = x
        self.y = y
    }
}

这样一来,之前的originpoint11就可以顺利通过编译了。总之一句话,为了让一个对象可以默认构造,class必须提供一个不需要参数的init方法,并且,这个方法必须初始化class的每一个属性。

在Swift里,这种真正初始化class属性的init方法,叫designated init,它们必须定义在class内部,而不能定义在extension里,否则会导致编译错误。

另外,除了designated init方法之外,还有一类不真正初始化class属性的方法,那它们是做什么的呢?

Convenience init

在实际编程中,除了使用memberwise的方式创建Point2D,我们还可能使用一些语义上更好的方式。例如:

let point22 = Point2D(at: (2.0, 2.0))

这时,我们就需要把作为参数的(2.0, 2.0)拆开成Point2D的每一个属性,然后调用designated init。对于完成这类任务的init方法,就叫做convenience init。

class Point2D {
    // ...
    convenience init(at: (Double, Double)) {
        self.init(x: at.0, y: at.1)
    }
}

可以看到,对于convenience init来说,它有两个要素:

  • 使用convienience关键字修饰;
  • 必须最终调用designated init完成对象的初始化;如果我们直接在convenience init中设置self.xself.y,会导致编译错误;

以上我们讨论的init方法有一个共性,就是它们的参数一定可以用来初始化class的属性,但事实并不总是如此。例如,对于convenience init来说,如果参数拆分后无法传递给designated init方法,这个init方法就会执行失败。为了处理这样的情况,Swift中还有一类init方法,叫做failable initiazlizer。

Failable init

例如,我们希望用一个String tuple初始化Point2D

let point44 = Point2D(at: ("4.0", "4.0"))

参考之前的convenience init,我们可以如法炮制一个:

class Point2D {
    // ...

    convenience init?(at: (String, String)) {
        guard let x = Double(at.0),
            let y = Double(at.1) else { 
                return nil 
            }
        
        self.init(at: (x, y))
    }
}

这次,由于String tuple版本的init有可能失败,我们需要用init?的形式来定义它。在它的实现里,如果参数中的String无法转换成Double,我们就返回nil,表示构建失败。否则,就调用Double tuple版本的convenience init最终完成对象的创建。

这里,我们只要保证最终可以调用到designated init方法就好了,而不一定要在convenience init方法中,直接调用deignated init方法。

另外要说明的一点是,一个failable designated init方法不能被non failable convenience init调用。但是,一个普通的designated init方法,却可以被failable convenience init调用。

最后,我们用一张图,来表示class init方法家族之间的关系:

Point2D init

What's next?

以上,就是单个class的各种init的用法和关系。但事情还远未结束,在下一节里,我们将会看到,当类之间存在继承关系的时候,为了保证派生类和基类的属性都可以被正确初始化,Swift定义了一套严格的two phase initialization机制。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们