此文章为本人翻译的译文,版权为原作者所有。
英文原文:Designated Initializers and Convenience Initializers in Swift
今天我们将了解有关类构造器的另一个部分,Swift 语言提供了两个不同类型的构造器,分别称为指定构造器和便利构造器,他们其实已经存在于 Objective-C 中,但到了 Swift 中它们有了点小变化,并且也引入了一个非常有用的关键字。下面我们将讨论一下这个话题,看看它们如何结合起来使你的类更加好用。
指定构造器
像 Swift 中大部分的构造器,指定构造器的名字就非常恰当地解释了它所做的工作。它们是一个类最主要的构造器。一个类必须有一个指定构造器,但并不是说只能有一个。如果必要,你可以声明多个,但是通常情况一个就足够了。
下面我们来简单看看指定构造器是什么样子:
init(sender: String, recipient: String) {
self.sender = sender
self.recipient = recipient
timeStamp = NSDate()
}
是不是看起来十分熟悉呢?我们之前有篇文章 Classes In Swift — An Introduction and Using a Nested Type in Swift (英文) 里介绍了一个 Message
类,这就是创建它的一个指定构造器的语法。我们的这个类只有一个构造器,所以它必须是指定构造器。创建它你只需要用到 init
这个关键字,配合几个需要用到的参数,就大功告成了。
便利构造器
便利构造器的作用也正如其名,他们就是用来让构造类的过程更简单。如果说指定构造器需要把所有的属性初始化好,并让用户自己传递这些属性的值,那便利构造器就是把其中几个属性硬编码了,这样我们就能用尽量少的参数去构造一个相同的对象了。通常,我们用便利构造器将对象用一些默认值进行初始化来让它们适合某种使用场景。
上面的构造器也许应该是一个便利构造器,因为它设置了 timeStamp
属性而没有让用户指定任何值。像我上面所说的,我认为指定构造器需要让用户传递所有需要设置的属性值(除非有些属性值是由其他参数值衍生计算而来的)。但这也只是个人偏好啦,上面那种写法作为指定构造器也是完全合法的,毕竟我们说 timeStamp
就是 Message
对象被构造的时间嘛。
尽管如此,让我们为我们的类添加一个便利构造器,让这个对象表示“它发送一个消息给它自己”。在这种情况下,我们就不需要传递两个不同的参数了,因为发送者就是接受者,所以我们声明一个只接受一个参数的便利构造器:
convenience init(sender: String) {
self.init(sender: sender, recipient: sender)
}
它们在语法上是不同的,便利构造器有个 convenience
关键字在 init
之前,其他方面到时非常相似。因为我们只取了一个参数,所以我们就把这一个参数分别传递给指定构造器的两个参数。
两种构造器的使用原则
Swift 有三个有关两种构造器相互使用的原则,这里我直接引用 Apple 的 iBook 原文,就不进行解释了:
- 一个指定构造器必须调用它直系父类的一个指定构造器。
- 一个便利构造器必须调用这个类自身的另一个构造器。
- 一个便利构造器最终一定会调用一个指定构造器。
引用自:Apple Inc. “The Swift Programming Language” iBooks
所以你能看到我们的便利构造器满足了上述的第 2 和 3 条原则。我们的指定构造器也处在类层次的最顶端,因此我们不需要调用父类的构造器(以满足第一天原则)。如果你重新调用了它,一定是它有一个子类,就像下面这个 TextMessage
类:
init(content: String, sender: String, recipient: String) {
self.content = content
super.init(sender: sender, recipient: recipient)
}
你看,它满足了第一条原则了。我们首先设置了这个类自身的 content
属性,然后调用了父类的指定构造器,这就完成这个类的初始化了。这也是和 Objective-C 的差异,我们在 Objective-C 中是先初始化父类,再设置自己的属性。当然,回到 Swift 中,如果需要你也可在调用完其父类后再设置继承来的属性。
根据 WWDC 的一个视频,这显然取决于类继承的工作方式。举个例子,如果一个父类调用了一个子类复写的方法,父类实际上将会调用子类复写后的方法实现(因为已经被复写了嘛),如果我们没有完全初始化好我们子类的属性,并且复写方法依赖于他们,那我们就遇到麻烦了。
这些原则有一些细微差别。在第二条原则中,一个便利构造器必须调用另一个构造器,其实不必是指定构造器,随便一个构造器都可以。如果你需要,你可以声明几个便利构造器,然后相互调用,这种链式调用时没问题的。但最终还是要符合第三条原则就对了。
举个例子吧:
convenience init() {
self.init(content: "")
}
convenience init(content: String) {
self.init(content: content, sender: "Myself")
}
convenience init(content: String, sender: String) {
self.init(content: content, sender: sender, recipient: sender)
}
你能看到无参便利构造器传递了一个默认参数调用了另外一个便利构造器,另一个又是如此,但最终指定构造器还是被调用了。
我们之前声明的便利构造器设置了相同的 sender
和 recipient
来达到某些目的,为什么不子类化一个新类来解决这个问题呢?那么...
class NoteMessage: Message {
let content: String
init(content: String, theUser: String) {
self.content = content
super.init(sender: theUser)
}
额,看来我们想当然了,别忘了第一条原则,一个指定构造器必须直接调用其直系父类的指定构造器。但我们尝试去调用它的一个便利构造器了,这看来是不行的。
我不知道为什么 Swift 设计成这样,毕竟一个便利构造器最终都会调用一个指定构造器。也许这迫使我们去思考最合适的默认属性值,而不是只顾便利。又或许以后这点会改变?谁知道呢。
总结 (译者按)
老外写文章有点啰嗦了,我这里也就不总结了,如果你认真阅读了本文,我相信你对指定构造器和便利构造器一定有了很清晰的认识。总的来说就是,便利构造器是为你类的初始化工作提供方便的,它们最终一定要依赖于那些真正使你类能正常工作的初始化工作,这也就是指定构造器的工作。