为啥在 Swift 中甚至需要方便关键字?

Posted

技术标签:

【中文标题】为啥在 Swift 中甚至需要方便关键字?【英文标题】:Why convenience keyword is even needed in Swift?为什么在 Swift 中甚至需要方便关键字? 【发布时间】:2015-09-02 23:06:26 【问题描述】:

由于 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")
    

【问题讨论】:

刚刚在文档中阅读了这一点,也对此感到困惑。 :// 【参考方案1】:

现有的答案只讲述了convenience 故事的一半。故事的另一半,即现有答案都没有涵盖的那一半,回答了 Desmond 在 cmets 中发布的问题:

为什么 Swift 会强迫我将 convenience 放在初始化器前面,因为我需要从中调用 self.init?`

我在this answer 中稍微提到了它,其中我详细介绍了几个 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) 怎么知道如何给Bar 添加的b 变量赋值?它没有。所以我们不能用一个不能初始化我们所有值的初始化器来初始化一个Bar实例。

这与convenience 有什么关系?

好吧,我们来看看the rules on initializer inheritance:

规则 1

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

规则 2

如果您的子类提供了其所有超类指定初始化器的实现(通过按照规则 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 初始化器。

【讨论】:

真正回答问题并遵循文档的唯一答案。如果我是 OP,我会接受。 你应该写一本书;) @SLN This answer 涵盖了很多关于 Swift 初始化程序继承如何工作的内容。 @nhgrif 为什么需要重新实现 所有 超类的指定初始化程序来解锁便利初始化程序?如果只重新实现了许多超类指定初始化器中的一个,编译器不能只解锁使用它的便利初始化器吗? @IanWarburton 我不知道这个“为什么”的答案。您在评论第二部分中的逻辑对我来说似乎是合理的,但文档清楚地表明这是它的工作原理,并且抛出一个您在 Playground 中询问的示例确认该行为与记录的内容相符。 【参考方案2】:

主要是清晰度。从你的第二个例子,

init(name: String) 
    self.name = name

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

var gender: 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()

【讨论】:

因此,即使没有 convenience 关键字,对于编译器来说,我正在尝试使用多个初始化器做什么也是非常明显的,但 Swift 仍然会对此感到烦恼。这不是我期望 Apple 提供的那种简单 =) 这个答案没有回答任何问题。你说的是“清晰”,但没有解释它是如何让事情变得更清晰的。【参考方案3】:

嗯,我首先想到的是它用于类继承中以提高代码组织和可读性。继续你的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 这个词

【讨论】:

去掉convenience 关键字后,Swift 不会获得足够的信息来以完全相同的方式运行吗? 不,如果你去掉convenience关键字你不能在没有任何参数的情况下初始化Employee对象。 具体来说,调用Employee() 调用(继承,由于convenience)初始化程序init(),它调用self.init(name: "Unknown")init(name: String),也是Employee 的便捷初始化器,调用指定的初始化器。【参考方案4】:

根据Swift 2.1 documentation,convenience 初始化器必须遵守一些特定规则:

    convenience 初始化器只能在同一个初始化器中调用 类,不在超类中(仅对,而不是向上)

    convenience 初始化程序必须调用指定的初始化程序 在链中的某个地方

    convenience 初始化程序无法更改之前的 ANY 属性 调用了另一个初始化器 - 而指定的初始化器 必须初始化当前类引入的属性 在调用另一个初始化程序之前。

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

【讨论】:

可以说,编译器可以在没有convenience 关键字的情况下解决这个问题。 此外,您的第三点具有误导性。便利构造器只能更改属性(并且不能更改let 属性)。它无法初始化属性。指定初始化器负责在调用super 指定初始化器之前初始化所有引入的属性。 至少方便关键字让开发人员清楚,它的可读性也很重要(加上检查初始化程序是否符合开发人员的期望)。你的第二点很好,我相应地改变了我的答案。【参考方案5】:

除了其他网友在这里解释的点之外,我的一点理解。

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

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

【讨论】:

为什么 Swift 会强迫我将 convenience 放在初始化程序前面,因为我需要从中调用 self.init?这似乎是多余的,有点不方便。【参考方案6】:

一个类可以有多个指定的初始化器。便利构造器是必须调用同一类的指定构造器的辅助构造器。

【讨论】:

以上是关于为啥在 Swift 中甚至需要方便关键字?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 private(set) 在 Swift 中不起作用?

为啥 Pydev 会给出内置关键字的语法错误?

为啥 doGet 不运行 processRequest?

为啥我初始化 uiview 和 uiview 也被 swift 删除?

为啥我们在 sql 中需要一个可选的关键字 OUTER? [复制]

为啥返回带有箭头函数的对象时,array.map 中需要 return 关键字?