翻译:swift 5初始化 被忽略的Convenience便捷初始化、Required和继承

初始化

初始化是准备使用的类,结构或枚举实例的过程。此过程涉及为该实例上的每个存储属性设置一个初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。

您可以通过定义初始值设定项来实现此初始化过程,初始值设定项类似于可以调用以创建特定类型新实例的特殊方法。与Objective-C初始值设定项不同,Swift初始值设定项不会返回值。它们的主要作用是确保首次使用类型之前,正确初始化类型的新实例。

类类型的实例还可以实现一个deinitializer,它在释放该类的实例之前执行任何自定义清除。有关反初始化程序的更多信息,请参见反初始化。

1. 设置存储属性的初始值

类和结构必须在创建该类或结构的实例时将其所有存储的属性设置为适当的初始值。存储的属性不能处于不确定状态。

您可以在初始化程序中为存储的属性设置初始值,或者通过将默认属性值分配为属性定义的一部分来设置初始值。以下各节介绍了这些操作。

注意

当您为存储的属性分配默认值,或在初始化程序中设置其初始值时,将直接设置该属性的值,而无需调用任何属性观察器。

1.1 初始化器

调用初始化程序以创建特定类型的新实例。最简单的形式是,初始化程序就像没有参数的实例方法,使用init关键字编写:

init() {
    // perform some initialization here
}

下面的示例定义了一个新结构,称为Fahrenheit存储以华氏度表示的温度。该Fahrenheit结构具有一个存储属性temperature,其类型为Double:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

该结构定义了一个init没有参数的初始化程序,该初始化程序使用值32.0(水的冰点,以华氏度为单位)初始化存储的温度。

1.2 默认属性值

您可以从初始化程序中设置存储属性的初始值,如上所示。或者,指定默认属性值作为属性声明的一部分。您可以通过在定义属性时为其分配初始值来指定默认属性值。

注意

如果属性始终采用相同的初始值,请提供默认值,而不要在初始化程序中设置值。最终结果是相同的,但是默认值将属性的初始化与其声明紧密联系在一起。它使初始化程序更短,更清晰,并使您能够从其默认值推断属性的类型。默认值还使您更容易利用默认初始化程序和初始化程序继承,如本章稍后所述。

您可以Fahrenheit通过temperature在声明属性时为其属性提供默认值,以更简单的形式从上面编写结构:

struct Fahrenheit {
    var temperature = 32.0
}

2 自定义初始化

您可以使用输入参数和可选属性类型,或通过在初始化期间分配常量属性来自定义初始化过程,如以下各节所述。

2.1 初始化参数

您可以提供初始化参数作为初始化程序定义的一部分,以定义自定义初始化过程的值的类型和名称。初始化参数具有与功能和方法参数相同的功能和语法。

以下示例定义了一个名为的结构Celsius,该结构存储以摄氏度表示的温度。该Celsius结构实现了两个自定义的初始化程序,称为init(fromFahrenheit:)和init(fromKelvin:),它们使用不同温度范围内的值初始化该结构的新实例:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

第一个初始化程序具有一个初始化参数,其参数标签为fromFahrenheit,参数名称为fahrenheit。第二个初始化程序具有一个初始化参数,其参数标签为fromKelvin,参数名称为kelvin。两个初始化程序都将其单个参数转换为相应的摄氏值,并将此值存储在名为的属性中temperatureInCelsius。

2.2 参数名称和参数标签

与函数和方法参数一样,初始化参数可以具有用于初始化程序主体的参数名称和用于调用初始化程序的参数标签。

但是,初始化程序在其括号前没有以函数和方法那样的方式标识函数的名称。因此,初始化器参数的名称和类型在确定应调用哪个初始化器中起着特别重要的作用。因此,如果您不提供初始化方法,则Swift会为初始化程序中的每个参数提供一个自动参数标签。

下面的例子定义了一个名为结构Color,具有三个恒定属性叫做red,green,和blue。这些属性在0.0和之间存储一个值,1.0以指示颜色中红色,绿色和蓝色的数量。

Color为初始化程序Double的红色,绿色和蓝色分量提供三个适当命名的类型的参数。Color还提供了带有单个white参数的第二个初始化器,该初始化器用于为所有三个颜色分量提供相同的值。

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

Color通过为每个初始化器参数提供命名值,可以使用这两个初始化器来创建新实例:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

请注意,如果不使用参数标签,则无法调用这些初始化程序。如果已定义参数标签,则必须始终在初始化程序中使用它们,而忽略它们是编译时错误:

let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required

2.3 不带参数标签的初始化参数

如果不想为初始化参数使用参数标签,请为该参数写下划线(_)而不是显式参数标签,以覆盖默认行为。

这是上述“初始化参数”中Celsius示例的扩展版本,带有一个附加的初始化程序,可使用已经在摄氏度范围内的值来创建新实例:CelsiusDouble

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0

初始化调用Celsius(37.0)的意图很明确,不需要参数标签。因此,应将此初始化程序编写为:可以通过提供未命名的值来调用它。init(_ celsius: Double)Double

2.4 可选属性类型

如果您的自定义类型具有逻辑上允许具有“无值”的存储属性(可能是因为在初始化期间无法设置其值,或者是因为稍后允许其具有“无值”),请使用可选类型。可选类型的属性将使用值自动初始化nil,表明该属性在初始化过程中故意具有“没有值”。

以下示例定义了一个名为的类SurveyQuestion,其具有一个可选String属性response:

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

直到询问问题后,才能知道对调查问题的回答,因此该response属性用String?或的类型声明为“可选String”。初始化nil新实例时,会自动为其指定默认值,表示“没有字符串” SurveyQuestion。

2.5 在初始化期间分配常量属性

您可以在初始化期间的任何时候为常量属性分配一个值,只要在初始化完成时将其设置为确定值即可。为常数属性分配值后,就无法再对其进行修改。

注意

对于类实例,只能在引入常量的类的初始化期间对其进行修改。子类不能修改它。

您可以SurveyQuestion从上面修改示例,以text对问题的属性使用常量属性而不是变量属性,以表明一旦SurveyQuestion创建了实例,问题就不会更改。即使该text属性现在是常量,仍可以在类的初始化程序中进行设置:

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

3 默认初始化器

迅速提供了一个默认初始值对于所有其属性提供缺省值,并且不提供至少一个初始值设定本身的任何结构或类。默认初始化程序仅创建一个新实例,并将其所有属性设置为其默认值。

本示例定义了一个名为的类ShoppingListItem,该类将购物清单中商品的名称,数量和购买状态封装起来:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

由于ShoppingListItem该类的所有属性都具有默认值,并且由于它是没有超类的基类,因此会ShoppingListItem自动获得一个默认的初始化器实现,该实现会创建一个将其所有属性都设置为其默认值的新实例。(该name属性是一个可选String属性,因此nil即使该值未写在代码中,它也会自动接收默认值。)上面的示例使用ShoppingListItem该类的默认初始化程序,使用初始化程序创建该类的新实例。语法,表示为ShoppingListItem(),并将此新实例分配给名为的变量item。

3.1 结构类型的成员初始化器

如果结构类型没有定义任何自己的自定义初始化程序,则它们会自动接收一个成员初始化程序。与默认初始化程序不同,该结构即使在存储了没有默认值的属性的情况下也会接收成员初始化程序。

逐成员初始化器是初始化新结构实例的成员属性的简便方法。可以通过名称将新实例的属性的初始值传递给成员初始化器。

下面的示例定义了一个结构Size,该结构具有两个称为width和的属性height。Double通过指定默认值,可以推断这两个属性均为类型0.0。

该Size结构会自动接收一个init(width:height:)成员级初始化程序,您可以使用该初始化程序来初始化新Size实例:

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

调用成员初始化程序时,可以忽略具有默认值的任何属性的值。在上面的示例中,该Size结构的height和width属性都具有默认值。您可以忽略一个属性或两个属性,并且初始化程序将对所有忽略的内容使用默认值,例如:

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"

4 值类型的初始化程序委托

初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委托,可避免在多个初始化程序之间重复代码。

对于值类型和类类型,初始化程序委派的工作方式以及允许何种形式的委派的规则是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委托过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。但是,类可以从其他类继承,如Inheritance中所述。这意味着类还有其他责任,以确保在初始化期间为它们继承的所有存储属性分配适当的值。这些职责在下面的类继承和初始化中进行了描述。

对于值类型,self.init在编写自己的自定义初始化程序时,通常使用同一值类型引用其他初始化程序。您self.init只能在初始化程序中调用。

请注意,如果您为值类型定义自定义初始化程序,则将不再有权使用该类型的默认初始化程序(或成员初始化程序,如果它是结构)。该约束防止了使用自动初始化程序之一的人意外绕过更复杂的初始化程序中提供的其他基本设置的情况。

注意

如果您希望自定义值类型可以使用默认的初始值设定项和成员明智的初始值设定项以及自己的自定义初始化项进行初始化,请在扩展名中编写自定义初始化项,而不是将其作为值类型的原始实现的一部分。有关更多信息,请参见扩展

以下示例定义了一个自定义Rect结构来表示几何矩形。该示例需要两个称为Size和的支持结构Point,这两个结构均为其0.0所有属性提供默认值:

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

您可以通过Rect以下三种方式之一来初始化下面的结构:使用其默认的零初始化值origin和size属性值,提供特定的原点和尺寸,或提供特定的中心点和尺寸。这些初始化选项由Rect结构定义中的三个自定义初始化器表示:

struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

如果结构没有自己的自定义初始化Rect程序,init()则第一个初始化程序在功能上与该结构将收到的默认初始化程序相同。此初始值设定项的主体为空,由一对空的花括号表示{}。调用此初始值设定项将返回一个Rect实例,该实例的originsize属性均使用和从其属性定义的默认值初始化:Point(x: 0.0, y: 0.0) Size(width: 0.0, height: 0.0)

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

第二个Rect初始化程序,init(origin:size:)在功能上与该结构如果没有自己的自定义初始化程序时将接收的成员初始化程序相同。此初始化程序仅将origin和size参数值分配给适当的存储属性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

第三个Rect初始化init(center:size:)器稍微复杂一些。首先根据一个center点和一个size值计算一个合适的原点。然后,它调用(或委托)init(origin:size:)初始化器,该初始化器将新的originsize值存储在适当的属性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

init(center:size:)初始化可能分配的新值origin,并size以相应的属性本身。但是,对于init(center:size:)初始化程序而言,利用已经提供了该功能的现有初始化程序更加方便(意图更清晰)。

注意

有关无需自行定义init()和init(origin:size:)初始化程序的另一示例编写方式,请参见Extensions

5 类继承和初始化

在初始化期间,必须为类的所有存储属性(包括该类从其超类继承的所有属性)分配一个初始值。

Swift为类类型定义了两种初始化器,以帮助确保所有存储的属性都接收初始值。这些被称为指定的初始化程序和便捷初始化程序。

5.1 指定的初始化程序和Convenience便利性初始化程序

指定的初始化器是类的主要初始化器。指定的初始化程序将完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类链中的初始化过程。

类往往只有很少的指定初始化器,而一个类只有一个很常见。指定的初始化程序是“漏斗”点,通过它们进行初始化,并通过该“漏斗”点在父类链中继续进行初始化过程。

每个类必须至少有一个指定的初始化程序。在某些情况下,可以通过从超类继承一个或多个指定的初始化程序来满足此要求,如下面的“自动初始化程序继承”中所述。

便利初始化器是辅助的,支持类的初始化器。您可以定义一个便捷初始化程序,以从与便捷初始化程序相同的类中调用一个指定初始化程序,并将某些指定初始值设定项的参数设置为默认值。您还可以定义一个便捷初始化程序,以针对特定用例或输入值类型创建该类的实例。

如果您的类不需要便利初始化器,则不必提供它们。只要通向通用初始化模式的快捷方式可以节省时间或使类的初始化更清晰,就可以创建便利的初始化器。

5.2 指定和便捷初始化程序的语法

指定的类初始化器的编写方式与值类型的简单初始化器的编写方式相同:

init(parameters) {
    statements
}

便捷初始化程序以相同的样式编写,但convenience修饰符放在init关键字之前,并用空格分隔:

convenience init(parameters) {
    statements
}

5.3 类类型的初始化程序委托

为了简化指定的初始化器和便捷的初始化器之间的关系,Swift将以下三个规则应用于初始化器之间的委托调用:

规则1
指定的初始值设定项必须从其直接超类调用指定的初始值设定项。
规则二
便利初始化程序必须从同一类调用另一个初始化程序。
规则三
便捷初始化程序必须最终调用指定的初始化程序。

记住这一点的一种简单方法是:

  • 指定的初始值必须始终委派了。
  • 便利的初始化必须始终委派跨越。

这些规则如下图所示:
在这里插入图片描述
在这里,超类具有一个指定的初始值设定项和两个便利的初始化项。一个便利初始化程序调用另一个便利初始化程序,后者又调用单个指定的初始化程序。这从上方满足规则2和3。超类本身没有其他超类,因此规则1不适用。

该图中的子类具有两个指定的初始化程序和一个便捷的初始化程序。便捷初始化程序必须调用两个指定的初始化程序之一,因为它只能调用同一类中的另一个初始化程序。这从上方满足规则2和3。两个指定的初始值设定项都必须从超类中调用单个指定的初始值设定项,以满足上方的规则1。

注意

这些规则不会影响您的类的用户如何创建每个类的实例。上图中的任何初始化程序都可用于创建它们所属类的完全初始化的实例。这些规则仅影响您如何编写类的初始化程序的实现。

下图显示了四个类的更复杂的类层次结构。它说明了此层次结构中指定的初始化程序如何充当类初始化的“漏斗”点,从而简化了链中各类之间的相互关系:
在这里插入图片描述

5.4 两阶段初始化

Swift中的类初始化是一个分为两个阶段的过程。在第一阶段,每个存储的属性都由引入它的类分配一个初始值。一旦确定了每个存储属性的初始状态,便开始第二阶段,并且在考虑将新实例准备就绪之前,每个类都有机会自定义其存储属性。

两阶段初始化过程的使用使初始化安全,同时仍为类层次结构中的每个类提供了完全的灵活性。两阶段初始化可防止在初始化属性值之前对其进行访问,并防止其他初始化程序意外地将属性值设置为其他值。

注意

Swift的两阶段初始化过程类似于Objective-C中的初始化。主要区别在于,在阶段1中,Objective-C为每个属性分配零或空值(例如0或nil)。Swift的初始化流程更加灵活,因为它可以让您设置自定义初始值,并且可以处理有效值0或nil无效值的类型。

Swift的编译器执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:
安全检查1
指定的初始化程序在将其委托给超类初始化程序之前,必须确保初始化其类引入的所有属性。
如上所述,仅在知道对象所有存储属性的初始状态后,才认为该对象的内存已完全初始化。为了满足此规则,指定的初始值设定项必须确保在传递链之前初始化其自身的所有属性。

安全检查2
在将值分配给继承的属性之前,指定的初始值设定项必须委托一个超类初始值设定项。如果不是这样,则指定的初始化器分配的新值将被超类覆盖,作为其自身初始化的一部分。
安全检查3
便利初始化程序必须在将值分配给任何属性(包括由同一类定义的属性)之前委托给另一个初始化程序。如果不是,便利初始化程序分配的新值将被其自己类的指定初始化程序覆盖。
安全检查4
在初始化self的第一阶段完成之前,初始化器无法调用任何实例方法,读取任何实例属性的值或将其称为值。
在第一阶段结束之前,该类实例并不完全有效。一旦在第一阶段结束时知道类实例是有效的,就只能访问属性,并且只能调用方法。

根据上述四个安全检查,以下是两阶段初始化如何进行:

阶段1

  • 指定的或便捷的初始化程序在类上调用。
  • 分配了该类新实例的内存。内存尚未初始化。
  • 该类的指定初始化程序确认该类引入的所有存储属性都具有值。现在已初始化这些存储属性的内存。
  • 指定的初始值设定项移交给超类初始值设定项,以为其自身的存储属性执行相同的任务。
  • 这将继续类继承链,直到到达链的顶部。
  • 一旦到达链的顶部,并且链中的最后一个类确保其所有存储的属性都具有值,则实例的内存被视为已完全初始化,并且阶段1已完成。

阶段2

  • 从链的顶部向下追溯,链中的每个指定的初始化程序都可以选择进一步自定义实例。初始化程序现在可以访问self并可以修改其属性,调用其实例方法,等等。
  • 最后,链中的所有便利初始化程序都可以选择自定义实例并使用self。

以下是第1阶段寻找假设的子类和超类的初始化调用的方式:
在这里插入图片描述
在此示例中,初始化始于对子类的便捷初始化程序的调用。此便捷初始化程序尚无法修改任何属性。它委托来自同一类的指定初始化器。

根据安全检查1,指定的初始化器确保子类的所有属性都有一个值。然后,它在其父类上调用指定的初始化器,以继续进行链上的初始化。

超类的指定初始化器确保所有超类属性都有一个值。没有其他可初始化的超类,因此不需要进一步的委派。

一旦超类的所有属性都具有初始值,就将其内存视为已完全初始化,并且阶段1已完成。

以下是第2阶段寻找相同初始化调用的方式:
在这里插入图片描述
现在,超类的指定初始化器有机会进一步自定义实例(尽管不必如此)。

一旦超类的指定初始值设定项完成,子类的指定初始值设定项即可执行其他自定义(尽管不必如此)。

最后,一旦子类的指定初始化程序完成,最初调用的便捷初始化程序就可以执行其他自定义。

5.5 初始化程序的继承和覆盖

与Objective-C中的子类不同,Swift子类默认情况下不会继承其超类初始化程序。Swift的方法可防止出现以下情况:超类中的简单初始化程序被更专门的子类继承,并用于创建未完全或正确初始化的子类的新实例。

注意

超类初始化程序在某些情况下会被继承,但是只有在安全且适当的情况下才可以这样做。有关更多信息,请参见下面的自动初始化继承

如果希望自定义子类提供与其父类相同的一个或多个初始化器,则可以在子类中提供这些初始化器的自定义实现。

当编写与超类指定的初始化程序匹配的子类初始化程序时,实际上是在提供该指定的初始化程序的替代。因此,必须override在子类的初始化程序定义之前编写修饰符。即使您要覆盖自动提供的默认初始化程序,这也是正确的,如Default Initializers中所述

与覆盖的属性,方法或下标一样,override修饰符的存在会提示Swift检查超类是否具有匹配的指定初始化器要被覆盖,并验证是否已按预期指定了覆盖初始化器的参数。

注意

override重写超类指定的初始值设定项时,您始终会编写修饰符,即使您的子类对初始值设定项的实现是便利的初始值设定项也是如此。

相反,如果您编写与超类便利性初始化程序匹配的子类初始化程序,则根据上面的“类类型的初始化程序委托”中所述的规则,该子类将永远无法直接调用该超类便利性初始化程序。因此,您的子类(严格地说)没有提供超类初始值设定项的替代。因此,override在提供超类便捷初始化程序的匹配实现时,您无需编写修饰符。

下面的示例定义了一个名为的基类Vehicle。此基类声明一个称为的存储属性numberOfWheels,默认Int值为0。该numberOfWheels属性由计算属性使用,该属性被称为description创建String车辆特性的描述:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Vehicle类提供了其唯一的存储属性的默认值,并没有提供任何自定义初始化本身。结果,它会自动接收一个默认的初始化程序,如Default Initializers中所述。默认的初始值设定项(如果有)始终是类的指定初始值设定项,可用于创建Vehicle带有的numberOfWheelsof的新实例0:

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

下一个示例定义了一个Vehicle名为的子类Bicycle:

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

该Bicycle子类定义指定初始化一个自定义的,init()。此指定的初始值设定项与的超类中的指定的初始值设定项匹配Bicycle,因此Bicycle此初始值设定项的版本标记有override修饰符。

init()初始值设定项Bicycle始于super.init(),该会调用Bicycle该类的超类的默认初始值设定项Vehicle。这样可以确保numberOfWheels继承的属性Vehicle在Bicycle被机会修改之前被初始化。调用后super.init(),的原始值将numberOfWheels替换为的新值2。

如果您创建的实例Bicycle,则可以调用其继承的description计算属性,以查看其numberOfWheels属性如何更新:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

如果子类初始化程序在初始化过程的第2阶段未执行任何自定义操作,并且超类具有零参数指定的初始化程序,则可以super.init()在将值分配给所有子类的所有存储属性后省略对的调用。

本示例定义的另一个子类Vehicle,称为Hoverboard。在其初始值设定项中,Hoverboard该类仅设置其color属性。super.init()该初始化程序没有显式调用,而是依靠对其父类的初始化程序的隐式调用来完成该过程。

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

的实例Hoverboard使用Vehicle初始化程序提供的默认轮子数量。

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

注意

子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。

5.6 自动初始化程序继承

如上所述,默认情况下,子类不继承其超类初始化程序。但是,如果满足某些条件,则会自动继承超类初始化器。实际上,这意味着您不需要在许多常见情况下编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承超类初始化程序。

假设您为子类中引入的任何新属性提供默认值,则适用以下两个规则:

规则1
如果您的子类没有定义任何指定的初始值设定项,它将自动继承其所有超类指定的初始值设定项。
规则二
如果您的子类提供了其所有超类指定初始化器的实现(通过按规则1继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有超类便利性初始化器。
即使您的子类添加了进一步的便捷初始化程序,这些规则也适用。

注意

子类可以将指定的超类初始化器实现为子类便捷性初始化器,作为满足规则2的一部分。

5.7 指定的便捷初始化器

下面的示例显示了指定的初始化程序,便捷初始化程序和自动初始化程序继承的运行方式。这个例子定义了三类所谓的层次Food,RecipeIngredient和ShoppingListItem,并演示了如何自己初始化互动。

层次结构中的基类称为Food,这是一个封装食品名称的简单类。该Food课程介绍一个String叫做物业name并提供两个初始化创建Food实例:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

下图显示了Food该类的初始化程序链:

在这里插入图片描述
类没有默认的成员初始化器,因此Food该类提供了一个指定的初始化器,该初始化器带有一个称为的参数name。该初始化程序可用于创建Food具有特定名称的新实例:

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

该类的初始值设定项作为指定的初始值设定项提供,因为它可确保新实例的所有存储属性都已完全初始化。本类没有超类,所以初始化不需要调用来完成初始化。

init(name: String)FoodFoodFoodinit(name: String)super.init()

该Food级还提供了一个方便的初始化,init()不带参数。该init()初始化通过委派跨越到一个新的食品提供了默认的占位符名称Food类的具有的价值:init(name: String)name[Unnamed]

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

层次结构中的第二类的子类Food叫RecipeIngredient。本RecipeIngredient类机型中的烹饪配方的成分。它引入了一个Int称为的属性quantity(除了name继承自的属性之外Food),并定义了两个用于创建RecipeIngredient实例的初始化程序:

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

下图显示了RecipeIngredient该类的初始化程序链:
在这里插入图片描述
该RecipeIngredient班有一个单一的指定初始化,,它可以用来填充的所有新的属性的实例。此初始化程序首先将传递的参数分配给属性,这是引入的唯一新属性。这样做之后,初始化器将委托给该类的初始化器。该过程满足上述两阶段初始化的安全检查1init(name: String, quantity: Int)

RecipeIngredient还定义了一个便利的初始化程序,该初始化程序仅用于按名称创建实例。对于所有创建的实例,该便利初始化程序均假定其数量为,而没有显式数量。此便捷初始化程序的定义使实例创建起来更快捷,更便捷,并且在创建多个单量实例时避免了代码重复。这种便利初始化跨越简单地委托类的指定初始化,传递一个值。init(name: String)RecipeIngredient

提供的便捷初始化程序采用与中指定的初始化程序相同的参数。因为此便捷初始化程序会覆盖其父类中的指定初始化程序,所以必须使用修饰符对其进行标记(如Initializer Inheritance和Overriding中所述)。init(name: String) RecipeIngredient init(name: String) Foodoverride

尽管RecipeIngredient将初始化程序提供为方便的初始化程序,但仍然提供了其超类的所有指定初始化程序的实现。因此,也会自动继承其超类的所有便利初始化程序。init(name: String)RecipeIngredient

在该示例中,超类为RecipeIngredientIS Food,其具有单一的方便称为初始化init()。因此,此初始值设定项由继承RecipeIngredient。init()函数的继承版本与版本完全相同Food,只不过它是委派给RecipeIngredient版本而不是版本。init(name: String)Food

这三个初始化程序均可用于创建新RecipeIngredient实例:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

层次结构中的第三和最后一类是一个子类RecipeIngredient叫ShoppingListItem。该ShoppingListItem级车型,因为它出现在购物清单配方成分。

购物清单中的每个项目都以“未购买”开始。为了表示这一事实,我们ShoppingListItem引入了一个布尔属性purchased,其默认值为false。ShoppingListItem还添加了计算description属性,该属性提供了ShoppingListItem实例的文本描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

注意

ShoppingListItem并未定义初始值设定purchased项来为提供初始值,因为购物清单中的商品(如此处建模)始终始于未购买的商品。

因为它为它引入的所有属性提供了默认值,并且本身未定义任何初始化程序,所以它会ShoppingListItem自动从其超类继承所有指定的初始化和便捷初始化程序。

下图显示了所有三个类的整体初始化程序链:
在这里插入图片描述
您可以使用所有继承的三个初始化器来创建一个新ShoppingListItem实例:

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘

在这里,breakfastList从包含三个新ShoppingListItem实例的数组文字中创建了一个名为的新数组。数组的类型推断为[ShoppingListItem]。创建阵列后,阵列ShoppingListItem开头的的名称从更改为"[Unnamed]",并标记为已购买。打印数组中每个项目的描述将显示它们的默认状态已按预期设置。“Orange juice”

6 初始化失败

有时定义初始化可能失败的类,结构或枚举有时很有用。无效的初始化参数值,缺少必需的外部资源或其他阻止初始化成功的条件可能触发此失败。

为了应对可能失败的初始化条件,请将一个或多个可失败的初始化程序定义为类,结构或枚举定义的一部分。您通过在init关键字(init?)后面放置问号来编写失败的初始化程序。

注意

您不能使用相同的参数类型和名称来定义可失败的初始化程序和不可失败的初始化程序。

失败的初始化程序会创建一个初始化类型的可选值。您在有故障的初始化程序中编写代码,以指示可以触发初始化失败的点。return nil

注意

严格来说,初始化器不返回值。相反,它们的作用是确保self在初始化结束时已完全正确地对其进行了初始化。尽管您编写触发初始化失败的代码,但是不要使用关键字来指示初始化成功。return nil

例如,为数字类型转换实现了失败的初始化器。为确保数字类型之间的转换准确地保留了该值,请使用init(exactly:)初始化程序。如果类型转换不能保持该值,则初始化程序将失败。

let wholeNumber: Double = 12345.0
let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {
    print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
}
// Prints "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int

if valueChanged == nil {
    print("\(pi) conversion to Int does not maintain value")
}
// Prints "3.14159 conversion to Int does not maintain value"

以下示例定义了一个名为的结构Animal,其常量String属性为species。该Animal结构还使用一个名为的单个参数定义了一个失败的初始化程序species。此初始化程序检查species传递给初始化程序的值是否为空字符串。如果找到空字符串,则会触发初始化失败。否则,将species设置属性的值,并且初始化成功:

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

您可以使用此失败的初始化程序尝试初始化新Animal实例,并检查初始化是否成功:

let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe"

如果将空字符串值传递给失败的初始值设定项的species参数,则初始值设定项将触发初始化失败:

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// Prints "The anonymous creature could not be initialized"

注意

检查空字符串值(例如"“而不是"Giraffe”)与进行检查nil以表明没有可选 String值的情况不同。在上面的示例中,空字符串("")是有效的,非可选的String。但是,让动物使用空字符串作为其species属性值是不合适的。为了对此限制建模,如果发现空字符串,则可失败的初始化程序将触发初始化失败。

6.1 枚举失败的初始化程序

您可以使用一个失败的初始化程序来基于一个或多个参数选择适当的枚举大小写。如果提供的参数与适当的枚举情况不匹配,则初始化器可能会失败。

下面的例子定义称为枚举TemperatureUnit,具有三种可能的状态(kelvin,celsius,和fahrenheit)。一个有故障的初始化器用于为Character代表温度符号的值找到合适的枚举形式:

enum TemperatureUnit {
    case kelvin, celsius, fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .kelvin
        case "C":
            self = .celsius
        case "F":
            self = .fahrenheit
        default:
            return nil
        }
    }
}

您可以使用此故障初始化程序为三种可能的状态选择合适的枚举用例,并在参数与以下状态之一不匹配时导致初始化失败:

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

6.2 带有原始值的枚举失败的初始化程序

带有原始值的枚举会自动接收一个失败的初始化器,init?(rawValue:)该初始化器采用称为rawValue合适原始值类型的参数,并在找到匹配的枚举情况下选择匹配的枚举情况,如果不存在匹配值,则触发初始化失败。

您可以TemperatureUnit从上面重写示例,以使用type的原始值Character并利用init?(rawValue:)初始化程序:

enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("This is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

6.3 初始化失败的传播

一个类,结构或枚举的故障初始化器可以委托同一类,结构或枚举的另一个故障初始化器。类似地,子类可故障初始化器可以委托最多超类可故障初始化器。

在任何一种情况下,如果委派给另一个导致初始化失败的初始化程序,则整个初始化过程将立即失败,并且不会再执行任何初始化代码。

注意

一个失败的初始化器也可以委派给一个不失败的初始化器。如果您需要将潜在的失败状态添加到不会失败的现有初始化过程中,请使用此方法。

以下示例定义了一个Product名为的子类CartItem。该CartItem级车型在在线购物车的商品。CartItem引入一个称为的存储常量属性,quantity并确保该属性始终具有至少一个值1:

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

失败的初始化程序CartItem通过验证其是否已接收到或更大的quantity值来启动1。如果quantity无效,则整个初始化过程将立即失败,并且不再执行任何初始化代码。同样,失败的初始化程序用于Product检查该name值,如果name为空字符串,则初始化程序进程立即失败。

如果CartItem使用非空名称创建实例并且数量等于1或大于0,则初始化成功:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"

如果尝试创建一个值为的CartItem实例,则初始化程序将导致初始化失败:quantity0CartItem

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// Prints "Unable to initialize zero shirts"

同样,如果您尝试CartItem使用空name值创建实例,则超类Product初始化程序会导致初始化失败:

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product"

6.4 覆盖失败的初始化程序

您可以在子类中覆盖超类可失败的初始化程序,就像其他任何初始化程序一样。或者,您可以使用子类不可失败的初始化程序来覆盖超类可失败的初始化程序。这使您可以定义一个子类,即使超类的初始化被允许失败,初始化也不会失败。

请注意,如果使用不可失败的子类初始化器覆盖了可失败的超类初始化器,则委托给超类初始化器的唯一方法是强制展开可失败的超类初始化器的结果。

注意

您可以使用不可失败的初始值设定项来覆盖可失败的初始设定项,但反之则不能。

下面的示例定义了一个名为的类Document。此类可为文档建模,该文档可以使用name非空字符串值或nil,但不能为空字符串的属性进行初始化:

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

下一个示例定义了一个Document名为的子类AutomaticallyNamedDocument。该AutomaticallyNamedDocument子类覆盖都是由引入的指定初始化的Document。这些覆盖可确保AutomaticallyNamedDocument实例的初始name值是,"[Untitled]"如果实例被初始化而没有名称,或者是否将空字符串传递给init(name:)初始化器:

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}

将AutomaticallyNamedDocument覆盖其超类的failable init?(name:)与nonfailable初始化init(name:)初始化。因为AutomaticallyNamedDocument以不同于其超类的方式处理空字符串情况,所以其初始化程序不需要失败,因此它提供了初始化程序的不可失败版本。

您可以在初始化器中使用强制展开来从超类中调用失败的初始化器,作为子类的非失败初始化器实现的一部分。例如,UntitledDocument下面的子类始终被命名为"[Untitled]",并且init(name:)在初始化期间使用其父类中的故障初始化器。

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}

在这种情况下,如果init(name:)曾经用空字符串作为名称调用超类的初始化程序,则强制展开操作将导致运行时错误。但是,由于使用字符串常量调用了它,因此可以看到初始化程序不会失败,因此在这种情况下不会发生运行时错误。

6.5 初始化!初始化失败

通常,您可以定义一个失败的初始化程序,该初始化程序通过在init关键字(init?)后面放置问号来创建适当类型的可选实例。另外,您可以定义一个失败的初始化程序,该初始化程序创建适当类型的隐式展开的可选实例。为此,可以在init关键字(init!)后面而不是问号旁放置一个感叹号。

您可以从委托init?到init!,反之亦然,你可以覆盖init?与init!反之亦然。您也可以从委托init到init!,但如果这样做会引发一个断言init!初始化原因初始化失败。

7 required必需的初始化器

required在类初始化器的定义之前编写修饰符,以指示该类的每个子类都必须实现该初始化器:

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

您还必须required在所需的初始化程序的每个子类实现之前编写修饰符,以指示初始化程序要求适用于链中的其他子类。override覆盖必需的指定初始值设定项时,您无需编写修饰符:

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

注意

如果可以通过继承的初始化程序满足要求,则不必提供所需的初始化程序的显式实现。

8 使用闭包或函数设置默认属性值

如果存储的属性的默认值需要一些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,都会调用闭包或函数,并将其返回值分配为属性的默认值。

这些类型的闭包或函数通常会创建与属性相同类型的临时值,定制该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。

这是有关如何使用闭包提供默认属性值的框架概述:

class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }()
}

请注意,闭包的大括号后面是一对空括号。这告诉Swift立刻执行关闭。如果省略这些括号,则尝试将闭包本身分配给属性,而不是闭包的返回值。

注意

如果使用闭包来初始化属性,请记住,在执行闭包时实例的其余部分尚未初始化。这意味着您无法从闭包内部访问任何其他属性值,即使这些属性具有默认值也是如此。您也不能使用隐式self属性,或调用任何实例的方法。

下面的示例定义了一个名为的结构Chessboard,该结构为国际象棋的棋盘建模。国际象棋在8 x 8的棋盘上进行游戏,黑白方块交替出现。
在这里插入图片描述
为了表示此游戏板,该Chessboard结构具有一个称为的属性boardColors,该属性是64个Bool值的数组。true数组中的值表示黑色正方形,而的值false表示白色正方形。阵列中的第一项代表板上的左上角正方形,而阵列中的最后一项代表板上的右下角正方形。

boardColors使用闭包初始化该数组以设置其颜色值:

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

每当Chessboard创建新实例时,都会执行闭包,并boardColors计算并返回默认值。上面的示例中的闭包在名为的临时数组中为板上的每个正方形计算并设置适当的颜色temporaryBoard,并在完成设置后将该临时数组作为闭包的返回值返回。返回的数组值存储在其中,boardColors并可以使用squareIsBlackAt(row:column:)实用程序函数进行查询:

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

参考

https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值