在Swift中,Two-Phase Initialization(二阶段初始化)是一种确保类对象在完全初始化之前不会被使用的机制。这个机制主要用于类的初始化,尤其是在继承体系中,以确保子类和父类的属性都正确地初始化。Two-Phase Initialization的设计避免了部分初始化的情况,确保对象总是处于一致的状态。
Two-Phase Initialization 的过程
第一阶段:确保所有存储属性都有初始值。
- 从最底层的子类开始,逐层向上初始化每一个类层级的存储属性,直到基类。
- 每个类初始化自己的属性后,调用其父类的初始化方法。
- 直到基类的存储属性全部初始化完成,第一阶段才算结束。
第二阶段:完成初始化,配置自定义设置。
5. 在所有属性都完成初始化后,开始从基类向下的逐层配置每一个类的自定义设置。
6. 在这个阶段,可以访问self
、调用实例方法和读取属性。
这种初始化顺序可以确保即使在复杂的类继承关系中,子类和父类都能得到正确的初始化,避免属性被访问时处于未初始化状态。
Swift编译器在Two-Phase Initialization过程中执行了四项安全检查,以确保初始化的完整性和正确性。这些安全检查帮助开发者避免常见的初始化错误,从而保证类对象的内存安全和一致性。
安全检查 1
指定初始化器(Designated Initializer)必须在向上代理到父类初始化器之前,确保它自身类中引入的所有属性都已初始化。
这项检查的原因是,只有当对象的所有存储属性都有了已知的初始值后,内存才算是完全初始化。指定初始化器需要确保它自己声明的所有属性都已经初始化,然后才能调用父类的初始化器。这可以避免在向上初始化过程中,父类的方法依赖于未初始化的子类属性,导致程序出错。
安全检查 2
指定初始化器必须在分配继承属性的值之前代理到父类初始化器。
这一点确保了指定初始化器不能先给继承的属性赋值后再调用父类初始化器,否则,父类的初始化过程可能会覆盖这个新赋的值。Swift通过这种检查避免了意外的属性覆盖问题,确保继承链中的初始化顺序和预期一致。
安全检查 3
便利初始化器(Convenience Initializer)必须在给任何属性赋值之前,调用另一初始化器。
便利初始化器的目的是简化指定初始化器的调用,但它不能直接初始化任何属性,包括本类中的属性。否则,便利初始化器赋的值会被指定初始化器的调用所覆盖。这样,Swift强制便利初始化器只能代理指定初始化器,以确保指定初始化器拥有对属性赋值的最终控制权。
安全检查 4
初始化器在第一阶段完成之前,不能调用实例方法、读取实例属性的值,或将
self
作为值引用。
因为在第一阶段结束前,对象尚未完全初始化,因此调用实例方法、访问实例属性,或使用self
可能会导致未定义行为。Swift的这个检查确保只有在所有属性都被赋值、对象的内存状态完全确定之后,才允许访问对象自身,从而避免了潜在的运行时错误。
示例代码
以下是一个示例,展示了如何在这些安全检查下正确初始化:
class Vehicle {var make: Stringinit(make: String) {self.make = make}
}class Car: Vehicle {var model: String// 安全检查 1 和 2init(make: String, model: String) {self.model = model // 自身属性 model 已初始化super.init(make: make) // 然后调用父类的初始化器}// 安全检查 3convenience init(model: String) {self.init(make: "DefaultMake", model: model) // 调用指定初始化器}// 安全检查 4:初始化完成后,才可以访问实例方法和属性func startEngine() {print("Engine started for \(make) \(model)")}
}let car = Car(model: "Sedan")
car.startEngine() // Engine started for DefaultMake Sedan
在这个例子中:
Car
的init(make:model:)
初始化器遵循了安全检查1和2。Car
的便利初始化器convenience init(model:)
遵循了安全检查3。startEngine()
方法在初始化完成后才能被调用,确保了安全检查4。
通过这些安全检查,Swift确保Two-Phase Initialization的过程安全且一致,使代码在复杂的继承体系中也能避免初始化顺序错误。
补充:关于安全检查第三条
在Swift中,初始化器的第一个阶段指的是所有存储属性的初始化,包括类自身定义的属性以及从父类继承的属性。编译器禁止在第一阶段完成之前调用实例方法、读取实例属性的值,或者将self
作为值引用,这个规则的目的是为了确保对象在完全初始化之前处于安全的状态。
原因与背景
在初始化的第一阶段,类的实例还没有完全初始化,某些属性的值可能尚未设置。因此,访问这些属性或使用self
可能会导致访问未初始化或不稳定的对象状态。为了避免这种风险,Swift强制要求初始化器在完成第一阶段(即所有属性都已初始化)之前,不能执行以下操作:
- 调用实例方法
- 读取实例属性的值
- 将
self
作为值
这些限制确保在类实例化的过程中,self
始终处于一个一致且已知的状态。
示例:访问未初始化的属性
假设我们有一个类,里面有一个初始化器,它在初始化阶段调用了一个实例方法:
class MyClass {var value: Intinit(value: Int) {self.value = valueself.printValue() // 错误:在第一阶段完成之前调用实例方法}func printValue() {print("Value is \(value)")}
}
这段代码会报错,原因是在init(value:)
初始化器中,尝试调用self.printValue()
,而此时self
的value
属性可能尚未初始化,因此编译器会禁止这种操作。
为什么禁止这种操作?
在第一阶段完成之前,类的存储属性并未完全初始化,可能会导致以下问题:
- 访问一个未初始化的属性会导致运行时错误,因为这个属性的值是未知的。
- 调用实例方法时,实例方法可能会访问尚未初始化的属性,从而触发不可预期的行为。
- 由于
self
指向的是一个未完全初始化的对象,尝试将self
作为值传递或引用也会导致不确定的状态。
什么时候可以使用self
?
一旦所有存储属性都被初始化,Swift会认为初始化器的第一阶段已经完成,初始化器进入第二阶段。这时,实例方法可以被调用,实例属性的值可以安全地访问,self
也可以作为值传递或引用。
例子:正确的初始化顺序
下面是一个正确的使用方式,初始化器不在第一阶段调用方法,直到所有属性都被初始化:
class MyClass {var value: Intvar description: Stringinit(value: Int, description: String) {self.value = valueself.description = descriptionself.printValue() // 正确:在初始化完成后调用实例方法}func printValue() {print("Value is \(value), Description is \(description)")}
}let myObject = MyClass(value: 10, description: "This is a test")
在这个例子中,printValue()
方法是在初始化阶段完成后调用的,此时value
和description
都已经被正确初始化。因此,这段代码是合法的。
总结
“An initializer can’t call any instance methods, read the values of any instance properties, or refer to self as a value until after the first phase of initialization is complete” 这条规则的核心目的是为了确保对象在初始化过程中始终处于安全和一致的状态。在初始化的第一阶段,类的存储属性尚未初始化,无法安全地访问实例的其他部分。只有在初始化器的第一阶段完成后,才可以安全地执行这些操作。