Swift 构造器的☝️思考

3,249 阅读4分钟

构造器,又叫初始化方法,想必大家都了解。无论是和 class 还是 struct 打交道,都逃不了初始化这一步骤。不过最近在回看 Swift 文档的时候,我发现了☝️之前不曾注意到的细节。

class ClassA {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class ClassB: ClassA {
    let age: Int
    init(name: String, age: Int) {
        super.init(name: name)
        self.age = age
    }
}

我们在写子类的初始化方法时,势必需要在其中调用父类的初始化方法。而上面的初始化方法,其实会报错,而报错内容是:

Property 'self.age' not initialized at super.init call

这个错误提示很简单,子类的属性必须要在父类的初始化方法调用之前初始化,或者说白一点,self.age = age 放到 super.init(name: name) 之前就好了。但是,大家有想过,为什么要这样吗?这条规则背后的设计逻辑是什么?

很多人首先会联想到 Objective-C 的构造方法

(instancetype)init
{
    self = [super init];
    if (self != nil) {
        self.name = @"Jack";
    }
    return self;
}

很明显,Objective-C 的初始化方法无一例外都首先调用了父类的构造方法,然后再初始化子类的属性,同时这也是符合开发人员直觉的。那为啥 Swift 偏偏不行呢?难道是 Swift 的特有机制导致的?

让我们看看 Swift 官方文档 是怎么说的。Swift 中的类初始化是一个分为两个阶段的过程。 这其中,编译器会执行四个有用的安全检查,以确保构造器完成时没有错误。这其中的第一条规则就是:

A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer.

指定的初始化程序必须确保其类引入的所有属性在委托给超类初始化程序之前都已初始化 。

而对于第一条规则的解释,虽然没有明确写在规则之后,但 Apple对于 两个阶段 四个安全检查 的最终目的是很明确的:

The use of a two-phase initialization process makes initialization safe, while still giving complete flexibility to each class in a class hierarchy. Two-phase initialization prevents property values from being accessed before they are initialized, and prevents property values from being set to a different value by another initializer unexpectedly.

两阶段初始化过程的使用使初始化安全,同时仍为类层次结构中的每个类提供了完全的灵活性。两阶段初始化可防止在初始化属性值之前对其进行访问,并防止其他初始化程序意外地将属性值设置为其他值。

这个解释似乎有些抽象,后来看到了一个博客举的一个例子,才恍然大悟。

class ClassA {
    let name: String
    init(name: String) {
        self.name = name
        description()
    }

    func description() {
        print("我已经初始化好啦,我的名字是: \(name)")
    }
}

class ClassB: ClassA {
    let age: Int
    init(name: String, age: Int) {
        self.age = age
        super.init(name: name)
    }

    override func description() {
        print("我已经初始化好啦,我的名字是: \(name), 我的年龄是: \(age)")
    }
}

思考一下,子类属性的初始化和父类的初始化方法的调用顺序,真的没有任何影响吗?假如我们先初始化父类的构造方法,然后再去给 age 赋值,结果会怎么样?

很明显,最终程序将无法运行,因为此时父类调用子类的 description 方法中, age 属性还尚未初始化。注意,由于 age 使用了 let 关键词修饰,因此其属性必须要在构造器中初始化。在构造器初始化 age 属性之前, age 的值都无法访问。无法访问的意思不是指 age 的值为 nil, 而是没有被初始化,即内存尚未被分配。这一点和 Objective-C 不同,因为在 Objective-C 中,属性声明后便有了初始值 nil ,因此即便在构造器中未对其初始化,也可以访问属性的值(nil)。对于这一点,Swift 文档也在其中进行了说明。

Swift’s two-phase initialization process is similar to initialization in Objective-C. The main difference is that during phase 1, Objective-C assigns zero or null values (such as 0 or nil) to every property. Swift’s initialization flow is more flexible in that it lets you set custom initial values, and can cope with types for which 0 or nil is not a valid default value.

Swift 的两阶段初始化过程类似于 Objective-C 中的初始化。主要区别在于,在阶段 1 中,Objective-C 为每个属性分配零或空值(例如 0 或 nil )。Swift 的初始化流程更加灵活,因为它可以让您设置自定义初始值,并且可以处理有效值 0 或 nil 无效值的类型。

正因为 Swift 的构造器更加灵活(允许不可变属性在构造器中初始化),也更加安全(属性需要在初始化后才能访问),因此也不难理解 Apple 对 Swift 的构造器有额外的要求。