引言
继续学习Swift文档,从上一章节:继承,我们学习了Swift继承相关的内容,如继承的作用、重写父类的方法和属性(override关键词)、防止重写(final关键词)等这些内容。现在,我们学习Swift的初始化相关的内容。由于篇幅较长,这里分篇来记录,接下来,Fighting!
这一章节内容有点冗长,理解即可,熟悉的朋友可以移步下一章节:析构函数
初始化
初始化是准备使用的类,结构体或枚举实例的过程。 此过程涉及为该实例上的每个存储属性设置一个初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。
您可以通过定义初始化程序来实现此初始化过程,初始化程序类似于可以调用以创建特定类型新实例的特殊方法。 与Objective-C初始化程序不同,Swift初始化程序不会返回值。 它们的主要作用是确保在首次使用类型之前,正确初始化类型的新实例。
类类型的实例还可以实现一个反初始化器,该初始化器将在释放该类的实例之前执行任何自定义清除。 有关Deinitialization的更多信息,请参见Deinitialization。
1 为存储属性设置初始值
类和结构体必须在创建该类或结构体的实例时将其所有存储的属性设置为适当的初始值。 存储的属性不能处于不确定状态。
您可以在初始化程序中为存储的属性设置初始值,也可以通过将默认属性值分配为属性定义的一部分来进行设置。 以下各节介绍了这些操作。
注意
当您为存储的属性分配默认值,或在初始化程序中设置其初始值时,将直接设置该属性的值,而无需调用任何属性观察器。
1.1 初始化方法
调用初始化方法以创建特定类型的新实例。 最简单的形式是,初始化方法就像没有参数的实例方法,使用init关键字编写:
init() {
// perform some initialization here
}
下面的示例定义了一个新的结构体,称为华氏温度,用于存储以华氏度表示的温度。 华氏结构具有一个存储的属性,即温度,其类型为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 默认属性值
您可以从初始化方法中设置存储属性的初始值,如上所示。 或者,在属性声明中指定默认属性值。 您可以通过在定义属性时为其分配初始值来指定默认属性值。
注意
如果属性始终使用相同的初始值,请提供默认值,而不要在初始化方法中设置值。 最终结果是相同的,但是默认值将属性的初始化与声明更紧密地联系在一起。 它使初始化方法更短,更清晰,并使您能够从其默认值推断属性的类型。 默认值还使您更容易利用默认初始化方法和初始化方法继承,如本章稍后所述。
您可以通过在声明温度属性时为其温度属性提供默认值的方式,从上方以更简单的形式编写华氏结构:
struct Fahrenheit {
var temperature = 32.0
}
2 自定义初始化
您可以使用输入参数和可选属性类型,或通过在初始化过程中分配常量属性来自定义初始化过程,如以下各节所述。
2.1 初始化参数
您可以在初始化方法定义的一部分中提供初始化参数,以定义自定义初始化过程的值的类型和名称。 初始化参数具有与函数和方法参数相同的功能和语法。
以下示例定义了一个称为摄氏温度的结构体,该结构体存储以摄氏度表示的温度。 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还提供了带有单个白色参数的第二个初始化方法,该初始化方法用于为所有三个颜色分量提供相同的值。
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示例的扩展版本,带有一个附加的初始化f方法,可根据已经在Celsius范围内的Double值创建一个新的Celsius实例:
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的类,并带有一个名为response的可选String属性:
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."
在询问问题之前,无法知道对调查问题的回答,因此,将响应属性声明为String?或“可选String”类型。 在初始化SurveyQuestion的新实例时,会自动为它分配默认值nil,表示“没有字符串”。
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 默认初始化方法
Swift为任何结构体或类提供了默认初始化方法,并且自身没有定义初始化方法。 默认初始化方法仅创建一个新实例,并将其所有属性设置为默认值。
本示例定义了一个名为ShoppingListItem的类,该类封装了购物清单中商品的名称,数量和购买状态:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
因为ShoppingListItem类的所有属性都具有默认值,并且由于它是没有父类的基类,所以ShoppingListItem自动获得一个默认的初始化方法实现,该实现会创建一个将其所有属性设置为其默认值的新实例。 (name属性是一个可选的String属性,即使该值未写在代码中,它也会自动接收默认值nil。)上面的示例对ShoppingListItem类使用默认的初始化方法来创建ShoppingListItem类的新实例。 具有初始化语法的类,编写为ShoppingListItem(),并将此新实例分配给名为item的变量。
3.1 结构体类型的成员初始化方法
如果结构体类型未定义任何自己的自定义初始化方法,则它们会自动收到一个成员初始化方法。 与默认初始化方法不同的是,该结构体即使存储了没有默认值的属性,也会收到成员初始化方法。
成员初始化方法是初始化新结构体实例的成员属性的简便方法。 可以通过名称将新实例的属性的初始值传递给成员初始化方法。
下面的示例定义了一个名为Size的结构,具有两个名为width和height的属性。 通过指定默认值0.0,可以推断这两个属性均为Double类型。
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 值类型的初始化方法委托
初始化方法可以调用其他初始化方法来执行实例初始化的一部分。此过程称为初始化方法委托,可避免在多个初始化方法之间重复代码。
对于值类型和类类型,初始化方法委派的工作方式以及允许哪种形式的委派的规则是不同的。值类型(结构体和枚举)不支持继承,因此它们的初始化方法委托过程相对简单,因为它们只能委托给自己提供的另一个初始化方法。但是,类可以从其他类继承,如继承中所述。这意味着类还有其他责任,以确保在初始化期间为它们继承的所有存储属性分配适当的值。这些职责在下面的类继承和初始化中进行了描述。
对于值类型,在编写自己的自定义初始化方法时,可以使用self.init引用同一值类型的其他初始化方法。您只能在初始化程序中调用self.init。
请注意,如果您为值类型定义自定义初始化方法,则将不再有权使用该类型的默认初始化方法(或成员初始化方法,如果它是结构体)。此约束防止了使用自动初始化方法之一的人意外绕过更复杂的初始化方法中提供的其他基本设置的情况。
注意
如果您希望自定义值类型可以使用默认的初始值设定项和成员级初始值设定项以及自己的自定义初始值设定项进行初始化,请在扩展名中编写自定义初始值设定项,而不是将其作为值类型原始实现的一部分。 有关更多信息,请参见Extensions。
以下示例定义了一个自定义的Rect结构体来表示一个几何矩形。 该示例需要两个支持的结构体,分别称为“大小”和“点”,这两个结构体的所有属性均提供默认值0.0:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
您可以通过以下三种方式之一来初始化Rect结构体:使用其默认的零初始化原点和尺寸属性值,提供特定的原点和尺寸,或提供特定的中心点和尺寸。 这些初始化选项由三个自定义的初始化方法表示,它们是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实例,该实例的origin和size属性均使用其属性定义中的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 :)稍微复杂一些。 首先根据中心点和大小值计算适当的原点。 然后,它调用(或委托)init(origin:size :)初始化方法,该初始化方法将新的origin和size值存储在适当的属性中:
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 :)初始化方法可能已将原始值和大小的新值分配给了适当的属性本身。 但是,init(center:size :)初始化方法比利用已经提供了该功能的现有初始化方法更为方便(意图更清晰)。
注意
有关无需亲自定义init()和init(origin:size :)初始化方法的另一示例编写方式,请参见Extensions。
5 类继承和初始化
在初始化期间,必须为类的所有存储属性(包括该类从其父类继承的所有属性)分配一个初始值。
Swift为类类型定义了两种初始化方法,以帮助确保所有存储的属性均接收初始值。 这些被称为指定的初始化方法和便捷初始化方法。
5.1 指定的初始化方法和便捷初始化方法
指定的初始化方法是类的主要初始化方法。 指定的初始化方法将完全初始化该类引入的所有属性,并调用适当的父类初始化方法以继续父类链中的初始化过程。
类往往只有很少的指定初始化方法,而一个类通常只有一个。 指定的初始化方法是“funnel”点,通过该“funnel”点进行初始化,并通过该“funnel”点继续父类链中的初始化过程。
每个类必须至少有一个指定的初始化方法。 在某些情况下,可以通过从父类继承一个或多个指定的初始化方法来满足此要求,如下面的Automatic Initializer Inheritance中所述。
便利的初始值设定项是辅助的,支持类的初始值设定项。 您可以定义一个便捷初始化方法,以从与便捷初始化方法相同的类中调用一个指定初始化方法,并将某些指定初始值设定项的参数设置为默认值。 您还可以定义一个便捷初始化方法,以针对特定用例或输入值类型创建该类的实例。
如果您的类不需要便利初始化方法,则不必提供它们。 只要通向通用初始化模式的快捷方式可以节省时间或使类的初始化更清晰,就可以创建方便的初始化方法。
5.2 指定的和便捷的初始化语法
指定的类初始化方法的编写方式与值类型的简单初始化方法的编写方式相同:
init(parameters) {
statements
}
便捷初始化程序以相同的样式编写,但是在便捷关键字init关键字之前放置了convenience修饰符,并用空格分隔:
convenience init(parameters) {
statements
}
5.3 类类型的初始化方法委托
为了简化指定初始化方法和便捷初始化方法之间的关系,Swift将以下三个规则应用于调用初始化方法之间的委托:
规则1
指定的初始值设定项必须从其直接父类调用指定的初始值设定项。
规则2
便捷初始化方法必须从同一个类里调用另一个初始化方法。
规则3
便利初始化方法必须最终调用指定的初始化方法。
记住这个的一个简便方法:
- 指定的初始化方法必须始终委托。
- 便捷初始化方法必须始终委派。
这些规则如下图所示:
在这里,父类具有一个指定的初始值设定项和两个便利的初始化项。 一个便利初始化方法调用另一个便利初始化方法,后者又调用单个指定的初始化方法。 这从上方满足规则2和3。 父类本身没有其他父类,因此规则1不适用。
该图中的子类具有两个指定的初始化方法和一个便捷的初始化方法。 便捷初始化方法必须调用两个指定的初始化方法之一,因为它只能调用同一类中的另一个初始化方法。 这从上方满足规则2和3。 两个指定的初始值设定项都必须从父类中调用单个指定的初始值设定项,以满足上方的规则1。
注意
这些规则不会影响classes用户如何创建每个class的实例。 上图中的任何初始化方法都可用于创建它们所属类的完全初始化的实例。 这些规则只会影响您如何编写类的初始化方法的实现。
下图显示了四个类的更复杂的类层次结构。 它说明了此层次结构中指定的初始化方法如何充当类初始化的“funnel”点,从而简化了链中各类之间的相互关系:
5.4 初始化的两个阶段
Swift中的类初始化是一个分为两个阶段的过程。 在第一阶段,每个存储的属性都由引入它的类分配一个初始值。 一旦确定了每个存储属性的初始状态,便开始第二阶段,并且在认为新实例可以使用之前,每个类都有机会自定义其存储属性。
两阶段初始化过程的使用使初始化安全,同时仍为类层次结构中的每个类提供了完全的灵活性。 两阶段初始化可防止在初始化属性值之前对其进行访问,并防止其他初始化方法意外地将属性值设置为其他值。
注意
Swift的两阶段初始化过程类似于Objective-C中的初始化。 主要区别在于,在阶段1中,Objective-C为每个属性分配零或空值(例如0或nil)。 Swift的初始化流程更加灵活,因为它可以让您设置自定义初始值,并且可以处理0或nil不是有效默认值的类型。
Swift的编译器会执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:
安全检查1
指定的初始值设定项必须确保由其类引入的所有属性在委托给父类初始值设定项之前都已初始化。
如上所述,仅在知道对象所有存储属性的初始状态后,才认为该对象的内存已完全初始化。 为了满足此规则,指定的初始值设定项必须确保在传递链之前初始化其自身的所有属性。
安全检查2
在将值分配给继承的属性之前,指定的初始值设定项必须委托一个父类初始值设定项。 如果不是这样,则指定的初始值设定项分配的新值将被父类覆盖,作为其自身初始化的一部分。
安全检查3
便利初始化方法必须在将值分配给任何属性(包括由同一类定义的属性)之前委托给另一个初始化方法。 如果不是,便利初始化方法分配的新值将被其自己class指定的初始化方法覆盖。
安全检查4
在初始化的第一阶段完成之前,初始化方法无法调用任何实例方法,读取任何实例属性的值或将self作为值。
在第一阶段结束之前,该类实例并不完全有效。 一旦在第一阶段结束时知道类实例是有效的,就只能访问属性,并且只能调用方法。
根据上述四项安全检查,以下是两阶段初始化的结果:
阶段1
- 在类上调用了指定的或便捷的初始化方法。
- 分配该类的新实例的内存。 内存尚未初始化。
- 该类的指定初始化方法确认该类引入的所有存储属性都具有值。
- 这些存储的属性的内存现在已初始化。
- 指定的初始值设定项移交给父类初始值设定项,以其自身的存储属性执行相同的任务。
- 这将继续类继承链,直到到达链的顶部。
- 一旦到达链的顶部,并且链中的最后一个类确保其所有存储的属性都具有值,则实例的内存将被视为已完全初始化,并且阶段1已完成。
阶段2
- 从链的顶部向下追溯,链中的每个指定的初始化方法都可以选择进一步自定义实例。 初始化方法现在可以访问self,并且可以修改其属性,调用其实例方法等等。
- 最后,链中的所有便利初始化方法都可以选择自定义实例并与self一起使用。
第1阶段查找假设的子类和父类的初始化调用的方式如下:
在此示例中,初始化始于对子类的便捷初始化方法的调用。 此便捷初始化方法尚无法修改任何属性。 它委托来自同一类的指定初始化方法。
根据安全检查1,指定的初始值设定项可确保子类的所有属性都有一个值。然后,它在其父类上调用指定的初始值设定项以继续链上的初始化。
父类的指定初始值设定项可确保所有父类属性都有一个值。 没有其他要初始化的父类,因此不需要进一步的委派。
一旦父类的所有属性都具有初始值,就将其内存视为已完全初始化,并且阶段1已完成。
第2阶段查找相同的初始化调用的方式如下:
现在,父类的指定初始化方法有机会进一步自定义实例(尽管不必如此)。
一旦父类的指定初始值设定项完成,子类的指定初始值设定项即可执行其他自定义操作(尽管同样,它不必这样做)。
最后,子类的指定初始化方法完成后,最初调用的便捷初始化方法可以执行其他自定义。
5.5 初始化方法的继承和重写
与Objective-C中的子类不同,Swift子类默认情况下不会继承其父类初始化方法。 Swift的方法可以防止这样的情况,即父类的简单初始化方法被更专门的子类继承,并用于创建未完全或正确初始化的子类的新实例。
注意
父类初始化方法在某些情况下会被继承,但是只有在安全且适当的情况下才可以这样做。 有关更多信息,请参见下面的Automatic Initializer Inheritance。
如果希望自定义子类提供一个或多个与其父类相同的初始化方法,则可以在子类中提供这些初始化方法的自定义实现。
当编写与父类指定的初始化方法匹配的子类初始化方法时,实际上是在提供该指定的初始化方法的替代。 因此,您必须在子类的初始值设定项定义之前编写override修饰符。 即使您要重写自动提供的默认初始化方法,也是如此,如默认初始化方法中所述。
与重写属性,方法或下标一样,override修饰符的存在会提示Swift检查父类是否具有匹配的指定初始化方法要被重写,并验证是否已按预期指定了重写初始化器的参数。
注意
重写父类指定的初始值设定项时,即使您子类的初始值设定实现是便捷的初始值设定项,您也始终要编写override修饰符。
相反,如果您编写与父类便利性初始化方法匹配的子类初始化方法,则根据上面的“类类型的初始化方法委托”中所述的规则,您的子类将永远无法直接调用该父类便利性初始化方法。 因此,您的子类(严格地说)没有提供父类初始值设定项的替代。 因此,在提供父类便捷初始化方法的匹配实现时,您无需编写override修饰符。
下面的示例定义了一个称为Vehicle的基类。 此基类声明一个称为numberOfWheels的存储属性,默认Int值为0。numberOfWheels属性由称为description的计算属性使用,以创建有关车辆特性的String描述:
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
Vehicle类为其唯一存储的属性提供默认值,并且本身不提供任何自定义初始化方法。 结果,它会自动接收默认初始化方法,如默认初始化方法中所述。 默认的初始值设定项(如果可用)始终是类的指定初始值设定项,可用于创建numberOfWheels为0的新Vehicle实例:
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修饰符标记。
Bicycle的init()初始值设定项始于调用super.init(),super.init()会为Bicycle类的父类Vehicle调用默认的初始值设定项。 这样可以确保在Bicycle可以修改属性之前,由Vehicle初始化numberOfWheels继承的属性。 调用super.init()之后,将numberOfWheels的原始值替换为新值2。
如果创建Bicycle的实例,则可以调用其继承的描述计算属性,以查看其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
如果您的子类没有定义任何指定的初始值设定项,它将自动继承其所有父类指定的初始值设定项。
规则2
如果您的子类提供了其所有父类指定初始化方法的实现(通过按规则1继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有父类便利初始化方法。
即使您的子类添加了进一步的便利初始化方法,这些规则也适用。
注意
子类可以将父类指定的初始化方法实现为子类便捷初始化方法,作为满足规则2的一部分。
5.7 指定的和便捷的初始化方法的实现
以下示例显示了实际的指定初始化方法,便捷初始化方法和自动初始化方法继承。 此示例定义了三个类的层次结构,分别称为Food,RecipeIngredient和ShoppingListItem,并演示了它们的初始化方法如何交互。
层次结构中的基类称为Food,它是封装食品名称的简单类。 Food类引入了一个名为name的String属性,并提供了两个用于创建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"
提供Food类的init(name:String)初始化方法作为指定的初始化方法,因为它可确保新Food实例的所有存储属性都已完全初始化。 Food类没有父类,因此init(name:String)初始化方法不需要调用super.init()即可完成其初始化。
Food类还提供了一个没有参数的便捷初始化方法init()。 init()初始值设定项委派给Food类的init(name: String),名称值为[Unnamed],从而为新食品提供默认的占位符名称:
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
层次结构中的第二个类是Food的子类,名为RecipeIngredient。 RecipeIngredient类对烹饪食谱中的成分进行建模。 它引入了一个称为数量的Int属性(除了它从Food继承的name属性之外),并定义了两个用于创建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类具有单个指定的初始化方法init(name:String,Quantity:Int),可用于填充新RecipeIngredient实例的所有属性。该初始化方法首先将传递的数量参数分配给数量属性,此属性是RecipeIngredient引入的唯一新属性。这样做之后,初始化方法将委托给Food类的init(name:String)初始化方法。该过程满足上述两阶段初始化的安全检查1。
RecipeIngredient还定义了一个便利的初始化程方法init(name:String),该初始化方法仅用于按名称创建RecipeIngredient实例。对于任何没有显式数量创建的RecipeIngredient实例,此便利初始化方法均假设其数量为1。此便捷初始化方法的定义使RecipeIngredient实例创建起来更快,更方便,并且在创建多个单量RecipeIngredient实例时避免了代码重复。此便捷的初始化方法仅将数量值1传递给该类的指定的初始化方法。
RecipeIngredient提供的init(name:String)便利初始化方法采用与Food中指定的init(name:String)初始化方法相同的参数。因为此便捷初始化方法会重写其父类中的指定初始化方法,所以必须使用override修饰符对其进行标记(如Initializer Inheritance and Overriding中所述)。
尽管RecipeIngredient提供了init(name:String)初始化方法作为便利的初始化方法,但RecipeIngredient仍提供了其所有父类指定的初始化方法的实现。因此,RecipeIngredient也会自动继承其所有父类的便利初始化方法。
在此示例中,RecipeIngredient的父类是Food,它具有一个名为init()的便捷初始化方法。因此,此初始值设定项由RecipeIngredient继承。 init()的继承版本的功能与Food版本完全相同,不同之处在于,它委派给init(name:String)的RecipeIngredient版本而不是Food版本。
这三个初始化方法均可用于创建新的RecipeIngredient实例:
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
层次结构中的第三个也是最后一个类是RecipeIngredient的子类,称为ShoppingListItem。 ShoppingListItem类对食谱成分进行建模,使其显示在购物清单中。
购物清单中的每个项目都以“未购买”开始。 为了表示这一事实,ShoppingListItem引入了一个布尔属性,称为purchaded,默认值为false。 ShoppingListItem还添加了一个计算的描述属性,该属性提供ShoppingListItem实例的文本描述:
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
注意
ShoppingListItem并未定义初始化方法来为购买的商品提供初始值,因为购物清单中的商品(如此处建模)始终始于未购买的商品。
因为它为它引入的所有属性提供了默认值,并且本身没有定义任何初始化方法,所以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 ✘
在这里,从包含三个新ShoppingListItem实例的数组文字创建了一个名为BreakfastList的新数组。 数组的类型推断为[ShoppingListItem]。 创建数组后,数组开头的ShoppingListItem的名称从“ [未命名]”更改为“橙汁”,并标记为已购买。 打印阵列中每个项目的描述将表明它们的默认状态已按预期设置。
6 初始化失败
有时,定义初始化可能失败的类,结构体或枚举有时很有用。 无效的初始化参数值,缺少必需的外部资源或其他阻止初始化成功的条件可能触发此失败。
为了应对可能失败的初始化条件,请将一个或多个可失败的初始化方法定义为类,结构体或枚举定义的一部分。 通过在init关键字(init?)后面放置问号,可以编写失败的初始化方法。
注意
您不能使用相同的参数类型和名称来定义可失败的初始化方法和不可失败的初始化方法。
失败的初始化方法会创建一个初始化类型的可选值。 您在失败的初始值设定项中写入return nil,以指示可以触发初始化失败的点。
注意
严格来说,初始方法不返回值。 相反,它们的作用是确保在初始化结束时完全正确地初始化自身。 尽管您编写了return nil来触发初始化失败,但是您并未使用return关键字来指示初始化成功。
例如,为数字类型转换实现了失败的初始化方法。 为了确保数值类型之间的转换可以准确地保持值,请使用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的单个参数。 此初始化方法检查传递给初始化方法的种类值是否为空字符串。 如果找到空字符串,则会触发初始化失败。 否则,将设置种类属性的值,并且初始化成功:
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值)不同。 在上面的示例中,空字符串(“”)是有效的非可选字符串。 但是,动物不宜使用空字符串作为其物种属性的值。 为了对此限制建模,如果发现空字符串,则可失败的初始化程序将触发初始化失败。
6.1 枚举初始化失败
您可以使用故障初始化方法基于一个或多个参数来选择适当的枚举用例。 如果提供的参数与适当的枚举case不匹配,则初始化方法可能会失败。
下面的示例定义了一个名为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
}
}
}
您可以使用此故障初始化方法为三种可能的状态选择合适的枚举case,并在参数与以下状态之一不匹配时导致初始化失败:
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示例,以使用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 初始化失败的传递
一个类,结构体或枚举的故障初始化方法可以委托同一个类,结构体或枚举的另一个故障初始化方法。 类似地,子类可故障初始化方法可以委托父类可故障初始化方法。
在任何一种情况下,如果委托给另一个导致初始化失败的初始化方法,则整个初始化过程将立即失败,并且不会执行其他初始化代码。
注意
一个失败的初始化方法也可以委派给一个不失败的初始化方法。 如果您需要将潜在的失败状态添加到不会失败的现有初始化过程中,请使用此方法。
下面的示例定义了一个名为CartItem的Product子类。 CartItem类为在线购物车中的商品建模。 CartItem引入了一个存储的常量属性,称为数量,并确保该属性的值始终至少为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的失败初始值设定项始于验证它收到的数量值为1或更大。 如果数量无效,则整个初始化过程将立即失败,并且不再执行任何初始化代码。 同样,Product的失败初始化方法将检查名称值,如果name为空字符串,则初始化方法将立即失败。
如果使用非空名称创建数量为1或更大的CartItem实例,则初始化成功:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"
如果尝试创建数量值为0的CartItem实例,则CartItem初始化程序将导致初始化失败:
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实例,则父类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属性初始化的文档进行建模,该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
}
}
下一个示例定义了一个名为AutomaticallyNamedDocument的Document子类。 AutomaticallyNamedDocument子类将重写Document引入的两个指定的初始化方法。 这些重写可确保如果实例初始化时不带名称,或者将空字符串传递给init(name :)初始化方法,则AutomaticallyNamedDocument实例的初始名称值为“ [Untitled]”:
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使用不可失败的init(name :)初始值设定项重写其父类的init可失败的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!进行委派,尽管如果init!这样做会触发一个断言。 初始化方法导致初始化失败。
7 必需的初始化方法
在定义类初始化方法之前编写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"
总结
这一章节内容有点冗长,翻译得有点费劲且不好理解,Swift的初始化的方法理解就可以了,有几个点要小结一下:
- convenience关键词的使用:定义便捷的初始化方法,在这个便捷初始化方法里,需要调用同一个类、结构体和枚举里的另一个初始化方法。
- 初始化失败的可选型:与声明可选属性的方式一样,在init后面加?(问好),表示这个初始化方法允许失败。
- 初始化失败强制解包:表示这个初始化方法返回必须有值,用init!表示。
- required关键词:修饰init方法,表示子类必须实现父类的init方法。
主要记住这几个点,其他的内容理解即可,文章内容冗长,看的头晕脑胀,可以不用花太多时间。最后,善良的你肯定会鼓励鼓励我的吧,谢谢啦~
参考文档: Swift - Initialization