您好!我叫 Kiryl Famin,是一名 iOS 开发人员。
今天,我想彻底研究一下 Swift 中的初始化器这一简单主题。尽管它看起来很简单,但有时对这一主题缺乏全面了解会导致令人沮丧的错误,人们希望快速修复这些错误,而不必深入研究细节。
在本文中,我们将介绍与初始化程序相关的所有内容,包括:
如何在定义自定义结构时保留结构的成员初始化程序
为什么并不总是需要在类中编写初始化程序
为什么在指定的初始化程序中并不总是需要调用super.init
为什么在调用super.init
之前必须填充子类的所有字段
如何在子类中以最少的覆盖访问所有父类初始化器
何时需要一个required
初始化程序
为什么UIView.init()
总是在没有参数的情况下被调用,但是init(frame:)
和init(coder:)
被覆盖
...还有更多。让我们一步一步来。
var
与let
required
初始化器:泛型、协议、 Self()
、 final
UIView()
苹果的指南《Swift 编程语言(6)》 (顺便说一句,其中对初始化器的描述出奇的详细)指出:
初始化是准备类、结构或枚举的实例以供使用的过程。此过程涉及为该实例上的每个存储属性设置初始值,并执行新实例可供使用之前所需的任何其他设置或初始化。
您可以通过定义初始化程序来实现此初始化过程,这些初始化程序类似于可以调用来创建特定类型的新实例的特殊方法。与 Objective-C 初始化程序不同,Swift 初始化程序不返回值。它们的主要作用是确保在首次使用之前正确初始化类型的新实例。
好吧,我想我不需要在这里添加任何内容。
我们先来讨论一下结构初始化器。由于没有继承,所以这很简单,但你仍然必须了解一些规则。
我们来写一个简单的结构:
struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)
请注意,我们无需显式声明初始化器即可初始化结构。发生这种情况是因为结构接收由编译器生成的成员初始化器。这仅适用于结构。
通过选择Refactor → Generate memberwise initializer ,你可以看到它的样子:
init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }
从签名中很容易看出,如果不为所有参数提供值,将导致编译错误:
let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call
但是,如果您想减少所需参数的数量,您可以定义自定义初始化程序:
init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅
请注意,如果isBlocked
未填充,这将导致编译错误,因为所有结构属性都必须在初始化程序中填充。
var
与let
唯一不需要明确填充字段的情况是当字段是可选( ?
)变量( var
)时。在这种情况下,该字段将默认为nil
:
struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅
但是,如果我们在这种情况下尝试使用成员初始化器,我们将收到编译错误:
let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call
发生这种情况的原因是声明自定义初始化器会删除成员初始化器。仍然可以显式定义它,但它不会自动可用。
但是,有一个保留成员初始化器的小技巧:在extension
中声明自定义初始化器。
struct BankAccount { let amount: Double var isBlocked: Bool? } extension BankAccount { init(amount: Double) { self.amount = amount } } let barclaysBankAccount = BankAccount(amount: 735) // ✅ let revolutBankAccount = BankAccount(amount: 812, isBlocked: false) // ✅ print(barclaysBankAccount.isBlocked) // nil print(barclaysBankAccount.isBlocked) // false
结构摘要
var
字段默认为nil
extension
中定义自定义初始化器类的主要初始化器是指定初始化器。它有两个用途:
class Animal { var name: String init(name: String) { self.name = name } } class Dog: Animal { var breed: String var name: String init(breed: String, name: String) { self.breed = breed super.init(name: name) } }
在调用super.init
之前必须填充所有字段。这是因为超类初始化程序可能会调用子类重写的方法,而这些方法可能会访问未填充的子类属性。
class Animal { var age: Int init(age: Int) { self.age = age getInfo() } func getInfo() { print("Age: ", age) } } class Dog: Animal { var breed: String init(breed: String, age: Int) { self.breed = breed // imagine we haven't done this super.init(age: age) } override func getInfo() { print("Age: ", age, ", breed: ", breed) } }
因此,如果我们没有设置self.breed = breed
,我们就会遇到运行时错误,因为Animal
初始化程序会从Dog
类调用重写的getInfo()
方法。此方法尝试访问breed
属性,但该属性尚未填充。
与结构不同,类不会接收隐式成员初始化器。如果存在未初始化的属性,则会发生编译错误:
class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
class Animal { // ✅ var age: Int = 0 }
class Animal { // ✅ var age: Int? }
class Animal { } // ✅
类也可以有一个便捷初始化器。与指定初始化器不同,它们不是从头开始创建对象,而是通过重用其他初始化器的逻辑来简化初始化过程。
class Rectangle { var width: Double var height: Double init(width: Double, height: Double) { self.width = width self.height = height } convenience init(side: Double) { self.init(width: side, height: side) // uses a designated initializer of self } }
便利初始化器可以调用指定初始化器或其他便利初始化器。最终,指定初始化器将始终被调用。
便利初始化器总是水平放置(self.init),而指定初始化器则垂直放置(super.init)。
一旦子类声明了新属性,它就无法访问所有超类的便利初始化程序。
class Animal { var age: Int var name: String init(age: Int, name: String) { self.age = age self.name = name } convenience init(age: Int) { self.init(age: age, name: "Default") } convenience init(name: String) { self.init(age: 0, name: name) } } class Dog: Animal { var breed: String init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } let dog = Dog(age: 3) // ❌ Missing arguments for parameters 'breed', 'name' in call
可以通过重写所有超类的指定初始化程序来解决此问题。
class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅
很容易看出,以这种方式,要在下一个子类中使用便捷初始化程序,您需要重写两个初始化程序。
class GuideDog: Dog { var isTrained: Bool override init(age: Int, name: String) { self.isTrained = false super.init(age: age, name: name) } override init(age: Int, name: String, breed: String) { self.isTrained = false super.init(age: age, name: name, breed: breed) } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅
但是,可以通过使用便捷覆盖初始化程序来避免这种情况。
class Dog: Animal { var breed: String convenience override init(age: Int, name: String) { self.init(age: age, name: name, breed: "Mixed") // self, not super } init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } class GuideDog: Dog { var isTrained: Bool // override init(age: Int, name: String) { // self.isTrained = false // // super.init(age: age, name: name, breed: "Mixed") // } convenience override init(age: Int, name: String, breed: String) { self.init(age: age, name: name, breed: breed, isTrained: false) // self, not super } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅
现在每个子类中只有 2 个明确指定的初始化器。
注意便捷重写初始化程序如何调用self
指定的 init 而不是super.init
。
我强烈推荐这本书,作者是 Tjeerd in 't Veen,他在《Swift in Depth》的第 5 章中详细解释了这个技巧。
super.init()
。我们已经讨论过,如果子类不引入新参数,它会自动继承所有超类的初始化器。
class Base { let value: Int init() { value = 0 } init(value: Int) { self.value = value } } class Subclass: Base { } let subclass = Subclass() // ✅ let subclass = Subclass(value: 3) // ✅
但是,还有一点很重要:如果超类只有一个指定的初始化器,并且它是无参数的(没有参数的init()
),那么子类中显式声明的初始化器不需要调用super.init()
。在这种情况下,Swift 编译器会自动插入对可用的无参数super.init()
调用。
class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }
代码编译通过,因为super.init()
被隐式调用。这对于以下一些示例至关重要。
在所有子类必须具有与基类相同的初始化程序的情况下,都使用required
初始化程序。它还必须调用super.init()
。以下是required
初始化程序的示例。
只有将泛型类型声明为必需的init
才能调用其required init
。
class Base { } class Subclass: Base { } struct Factory<T: Base> { func initInstance() -> T { // ❌ Constructing an object of class T() // type 'T' with a metatype value } // must use a 'required' initializer }
这段代码无法编译,因为Factory
对Base
的子类一无所知。虽然在这个特殊情况下, Subclass
确实有一个没有参数的init()
,但想象一下如果它引入了一个新字段:
class Subclass: Base { let value: Int init(value: Int) { self.value = value } }
这里,它不再具有空的init
,因此必须按required
进行声明。
class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()
请注意,即使我们没有在Subclass
中明确声明required init
,编译器也会为我们生成它。这在Compiler Assistance中进行了讨论。 required init
被自动继承并称为super.init()
。
class Subclass: Base { required init() { super.init() } }
协议中声明的所有初始化程序都必须是required
:
protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }
再次强调,这是必要的,因为编译器可以确保子类实现协议初始化器。我们已经知道,这并不总是会发生 — 如果init
不是required
,子类就没有义务覆盖它,并且可以定义自己的初始化器。
class IntValue: InitableObject { let value: Int init(value: Int) { self.value = value } } let InitableType: Initable.Type = IntValue.self let initable: Initable = InitableType.init()
当然,下面的代码将无法编译,因为required
Base.init()
。
class InitableObject: Initable { required init() { } // ✅ } class IntValue: InitableObject { let value: Int required init() { self.value = 0 } init(value: Int) { self.value = value } }
在静态方法中调用Self()
初始化程序时也会发生类似的情况。
class Base { let value: Int init(value: Int) { self.value = value } static func instantiate() -> Self { Self(value: 3) // ❌ Constructing an object of class type 'Self' } // with a metatype value must use a 'required' initializer }
一如既往,问题在于继承:
class Subclass: BaseClass { let anotherValue: Int init(anotherValue: Int) { self.anotherValue = anotherValue } } let subclass = Subclass.instantiate() // ❌
class BaseClass { let value: Int required init(value: Int) { // ✅ self.value = value } static func instantiate() -> Self { Self(value: 3) } }
required
: final
由于required
的目的是强制在子类中实现初始化程序,因此,自然地,使用final
关键字禁止继承就不需要将初始化程序标记为required
。
protocol Initable { init() } final class InitableObject: Initable { } // ✅
protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }
init()
,则会在子类初始化程序中自动调用它。required
初始化程序来保证其存在于子类中以供泛型、协议和Self()
使用。简要提到了没有参数的UIView()
初始化程序,它在文档中找不到,但却在各处神秘地使用。
原因是UIView
继承自NSObject
,而 NSObject 有一个无参数的init()
。因此,这个初始化器没有在UIView
接口中明确声明,但它仍然可用:
@available(iOS 2.0, *) @MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate { open class var layerClass: AnyClass { get } public init(frame: CGRect) public init?(coder: NSCoder) open var isUserInteractionEnabled: Bool // no init()
然而,在底层,此初始化程序在代码中初始化时会调用init(frame:)
在通过 Interface Builder 初始化时会init(coder:)
。发生这种情况是因为UIView
提供了自己的NSObject.init()
实现,这一点可以通过method_getImplementation
返回NSObject.init()
和UIView.init()
的不同地址这一事实得到证实。
可失败的初始化只是返回一个可选的
final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }
具有原始值的枚举可获得免费的init?(rawValue:)
enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")
您还可以为枚举创建自定义初始化。所有枚举初始化都必须赋值self
。
enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }
我们已经介绍了 Swift 中初始化程序的所有基本方面:
在初始化程序中,所有字段都必须填充。
可选的var
属性默认为nil
。
结构接收一个免费的成员初始化器。
当定义自定义初始化程序时,成员初始化程序就会消失。
指定的初始化程序确保所有字段都已填充并调用super.init()
。
便捷初始化程序通过调用指定初始化程序来简化初始化。
便利初始化器总是水平放置( self.init
),而指定初始化器则垂直放置( super.init
)。
如果子类声明了新属性,则便捷初始化程序将无法使用。
要恢复超类的便捷初始化器,必须重写其所有指定的初始化器。
为了最大限度地减少覆盖的次数,可以使用便捷覆盖初始化程序。
如果子类没有引入新的参数,它会自动从其超类继承所有初始化器。
如果超类只有没有参数的init()
,则会在子类初始化程序中自动调用它。
必需的初始化程序确保其存在于子类中以供泛型、协议和Self()
使用。
UIView.init()
调用UIView.init(frame:)
或UIView.init(coder:)
。
可失败的初始化程序返回一个可选项。
具有原始值的枚举可获得免费的init?(rawValue:)
。
所有枚举初始值设定项都必须赋值self
。
希望本文对您有所帮助。如果您还有任何不清楚的地方或发现不准确之处,请随时通过 Telegram 联系我获取免费解释: @kfamyn 。