为什么在Swift中甚至需要便捷关键字?


132

由于Swift支持方法和初始化方法的重载,因此您可以将多个方法init并排放置,并使用您认为方便的方法:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

那么,为什么convenience关键字甚至存在呢?是什么使以下内容显着改善?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}

13
只是在文档中仔细阅读过,对此也感到困惑。:/
boidkan 2015年

Answers:


235

现有的答案只说明了一半convenience。故事的另一半(现有答案中没有涵盖的另一半)回答了Desmond在评论中发布的问题:

为什么Swift会convenience因为我需要self.init从其调用而迫使我放在初始化器的前面?

我在此答案中略有涉及,其中详细介绍 Swift的一些初始化规则,但主要重点是required单词。但是该答案仍在解决与该问题和答案有关的问题。我们必须了解Swift初始化程序继承的工作原理。

由于Swift不允许使用未初始化的变量,因此不能保证从继承的类继承所有(或任何)初始化器。如果我们将其子类化并向其子类中添加任何未初始化的实例变量,那么我们将停止继承初始化器。在添加我们自己的初始化程序之前,编译器会大吼大叫。

需要明确的是,未初始化的实例变量是任何未赋予默认值的实例变量(请注意,可选变量和隐式解包的可选变量会自动采用默认值nil)。

因此,在这种情况下:

class Foo {
    var a: Int
}

a是未初始化的实例变量。除非提供a默认值,否则不会编译:

class Foo {
    var a: Int = 0
}

a使用初始化方法进行初始化:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

现在,让我们看看如果继承了子类Foo,会发生什么情况?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

对?我们添加了一个变量,并添加了一个初始值设定项以将其设置为一个值,b以便对其进行编译。根据您使用的语言,您可能希望它Bar继承了Foo的初始值设定项init(a: Int)。但事实并非如此。怎么可能呢?怎样Fooinit(a: Int)诀窍分配给一个数值b变量Bar添加?没有。因此,我们无法Bar使用无法初始化所有值的初始化程序来初始化实例。

这些有什么关系convenience

好吧,让我们来看一下初始化程序继承的规则

规则1

如果您的子类没有定义任何指定的初始化器,它将自动继承其所有超类指定的初始化器。

规则二

如果您的子类提供了其所有超类指定的初始化器的实现(通过按照规则1继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有超类便利性的初始化器。

注意规则2,其中提到了便利初始化程序。

因此,convenience关键字的作用是向我们指示可以由添加没有默认值的实例变量的子类继承哪些初始化程序

让我们来看这个示例Base类:

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

注意,convenience这里有三个初始化程序。这意味着我们有三个可以继承的初始化程序。我们有一个指定的初始化程序(一个指定的初始化程序就是任何不是便利初始化程序的初始化程序)。

我们可以通过四种不同的方式实例化基类的实例:

在此处输入图片说明

因此,让我们创建一个子类。

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

我们继承自Base。我们添加了自己的实例变量,但没有给它提供默认值,因此我们必须添加自己的初始化程序。我们添加了一个,init(a: Int, b: Int, c: Int)但它与Base该类的指定初始值设定项的签名不匹配init(a: Int, b: Int)。这意味着,我们不会从继承任何初始化程序Base

在此处输入图片说明

因此,如果我们继承自Base,会发生什么,但是我们继续实现了与指定的初始值设定项匹配的初始值设定项Base

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

现在,除了我们在该类中直接实现的两个初始化器之外,由于我们实现了与Base类的指定初始化器匹配的初始化器,因此我们继承了Base该类的所有convenience初始化器:

在此处输入图片说明

具有匹配签名的初始化器标记为的事实在convenience这里没有区别。这仅意味着Inheritor只有一个指定的初始化程序。因此,如果我们继承自Inheritor,则只需实现一个指定的初始值设定项,然后继承Inheritor的便利初始化器,这又意味着我们已经实现了所有Base的指定的初始值设定项,并且可以继承其convenience初始值设定项。


16
真正回答问题并遵循文档的唯一答案。如果我是OP,我会接受的。
FreeNickname

12
您应该写一本书;)
coolbeet 2016年

1
@SLN 这个答案涵盖了很多关于Swift初始化程序继承的工作方式。
nhgrif

1
@SLN因为使用创建Bar init(a: Int)会保持b未初始化状态。
伊恩·沃伯顿

2
@IanWarburton我不知道这个特殊“为什么”的答案。在我的第二部分中,您的逻辑似乎对我来说很合理,但是文档清楚地表明了它的工作原理,并在Playground中举一个您要询问的内容的示例,以确认行为与记录的内容相符。
nhgrif

9

主要是清晰度。从第二个例子来看,

init(name: String) {
    self.name = name
}

是必需的或指定的。它必须初始化所有常量和变量。便利的初始化器是可选的,通常可用于使初始化更加容易。例如,假设您的Person类具有可选的可变性别:

var gender: Gender?

性别是一个枚举

enum Gender {
  case Male, Female
}

你可以有这样的便利初始化器

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

便捷初始化程序必须在其中调用指定的或必需的初始化程序。如果您的类是子类,则必须super.init() 在其初始化内调用。


2
因此对于编译器来说,即使没有convenience关键字,我正在尝试使用多个初始化程序也将是显而易见的,但是Swift仍然会对此进行调试。那不是我期望的那种简单的苹果=)
Desmond Hume 2015年

2
这个答案什么也没回答。您说的是“澄清度”,但没有说明它如何使事情变得更清晰。
Robo Robok

7

好吧,我首先想到的是,它在类继承中用于代码的组织和可读性。继续Person上课,想一想这样的情况

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

使用便捷初始化程序,我可以创建一个Employee()没有值的对象,因此单词convenience


2
随着convenience关键字带走,不会斯威夫特得到足够的信息完全相同的方式来表现?
Desmond Hume 2015年

不,如果您带走了convenience关键字,那么就不能在Employee没有任何参数的情况下初始化对象。
u54r 2015年

具体来说,调用会Employee()调用(由于而继承convenience)初始化程序init(),后者会调用self.init(name: "Unknown")init(name: String),也是的便捷初始化程序Employee,调用指定的初始化程序。
BallpointBen

1

除了其他用户在这里解释的要点外,我的理解也很深。

我强烈感到便捷初始化程序和扩展之间的联系。对于我来说,方便的初始化器在我想修改(在大多数情况下,使之简短或简单)现有类的初始化时最有用。

例如,您使用的某个第三方类init具有四个参数,但是在您的应用程序中,后两个具有相同的值。为了避免更多的输入并使代码干净,可以定义convenience init仅包含两个参数的a ,并在其内部调用self.init带有默认值的last作为参数。


1
为什么Swift会convenience因为我需要self.init从其调用而迫使我放在初始化器的前面?这似乎是多余的,并且有点不方便。
Desmond Hume

1

根据Swift 2.1文档convenience初始化程序必须遵守一些特定规则:

  1. 一个convenience初始化只能调用intializers在同一个班级,而不是在超级类(只跨不起来)

  2. 一个convenience初始化符调用指定的初始化链中的某处

  3. 一个convenience初始化不能改变任何,而指定的初始化-它调用之前另一个初始化属性 必须调用另一个初始化之前初始化由当前类引入的属性。

通过使用convenience关键字,Swift编译器知道它必须检查这些条件,否则就不必这样做。


可以说,编译器可能不用convenience关键字就可以解决此问题。
nhgrif

此外,您的第三点令人误解。便捷初始化程序只能更改属性(而不能更改let属性)。它无法初始化属性。指定的初始化程序负责在调用super指定的初始化程序之前初始化所有引入的属性。
nhgrif

1
至少便捷关键字对开发人员来说很清楚,可读性也很重要(还要对照开发人员的期望检查初始化程序)。您的第二点很好,我相应地更改了答案。
TheEye '16

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.