第14章:初始化
注意
在Swift中类、结构体、枚举的初始化是比较复杂的,涉及的内容也较多,请仔细阅读。
初始化是创建要使用的类,结构或枚举的实例的过程。此过程涉及为该实例上的每个存储属性设置初始值,并执行在新实例准备好使用之前所需的任何其他设置或初始化。
可以自定义这个初始化过程,这就像特殊的方法,可以被调用来创建特定类型的新实例。与Objective-C初始化不同,Swift初始值设定项不返回值。它们的主要作用是确保在第一次使用之前正确初始化类型的新实例。
14.1 设置存储属性的初始值
类类型的实例也可以实现反初始化,它在释放该类的实例之前执行任何自定义清理。
初始化最重要的一项工作就是正确的设置存储属性的值。在创建该类或结构的实例时,类和结构必须将其所有存储的属性设置为适当的初始值,存储属性不能保留在不确定的状态。可以在初始化程序中为存储的属性设置初始值,也可以通过将默认属性值指定为属性定义的一部分来设置初始值。
注意
将默认值分配给存储属性或在初始化程序中设置其初始值时,将直接设置该属性的值,而不调用任何属性观察者。
14.1.1 通过初始化方法设置存储属性的值
// 最简单形式的初始化方法
init() {
// perform some initialization here
}
// 通过初始化方法为存储属性设置初始值的实例
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"
14.1.2 通过默认值设置存储属性的值
struct Fahrenheit {
// 在这里已经设置了默认值,在创建其实例时会自动使用该默认值
// 由于该结构只有一个存储属性,所以不需要再编写初始化方法
var temperature = 32.0
}
注意
如果属性始终采用相同的初始值,请提供默认值,而不是在初始值设定项中设置值。最终结果是相同的,但默认值将属性的初始化与其声明更紧密地联系在一起。它使初始化程序更短,更清晰,并使您能够从其默认值推断属性的类型。
14.2 自定义初始化
你可以使用输入参数和可选属性类型自定义初始化过程,或者在初始化期间分配常量属性
,如以下部分所述。
14.2.1 初始化参数
你可以提供初始化参数作为初始化程序定义的一部分,以定义自定义初始化过程的值的类型和名称。初始化参数具有与函数和方法参数相同的功能和语法。
以下示例定义了一个名为Celsius的结构,该结构存储以摄氏度表示的温度。该Celsius结构实现了两个称为指定初始化的方法,其初始化从不同的温度刻度的值结构的新实例:
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。两个初始值设定项都将其单个参数转换为相应的Celsius值,并将此值存储在名为的属性中temperatureInCelsius。
14.2.2 参数名称与参数标签
与函数和方法参数一样,初始化参数既可以具有在初始化程序体内使用的参数名称,也可以具有在调用初始化程序时使用的参数标签。
但是,初始化程序在其括号之前没有函数和方法的标识函数名称
。因此,初始化程序参数的名称和类型在识别应该调用哪个初始化程序时起着特别重要的作用。因此,如果您不提供初始化程序,Swift会为初始化程序中的每个参数提供自动参数标签
。
下面的例子定义了一个名为结构Color,具有三个恒定属性叫做red,green,和blue。这些属性在两者之间存储一个值0.0,1.0用于指示颜色中红色,绿色和蓝色的数量。
Color为初始化程序提供了三个适当命名Double的红色,绿色和蓝色组件类型参数。Color还提供了具有单个white参数的第二个初始化器,用于为所有三个颜色组件提供相同的值。
struct Color {
let red, green, blue: Double
/*
如果没有指定参数标签,编译器会自动创建与参数名相同的标签,所以以下声明的完整形式如下
init(red red: Double, green green: Double, blue 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
14.2.3 没有参数标签的初始化参数
如果不想对初始化参数使用参数标签,请为该参数编写下划线(_
)。
1. struct Celsius {
2. var temperatureInCelsius: Double
3. init(fromFahrenheit fahrenheit: Double) {
4. temperatureInCelsius = (fahrenheit - 32.0) / 1.8
5. }
6. init(fromKelvin kelvin: Double) {
7. temperatureInCelsius = kelvin - 273.15
8. }
9. init(_ celsius: Double) {
10. temperatureInCelsius = celsius
11. }
12. }
13. let bodyTemperature = Celsius(37.0)
14. // bodyTemperature.temperatureInCelsius is 37.0
初始化程序调用Celsius(37.0)
在其意图中是明确的,无需参数标签。因此,编写此初始化程序是合适的,以便可以通过提供未命名的值来调用它。
14.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”。
// 当SurveyQuestion初始化新实例时,会自动为其分配默认值(nil),意味着“还没有字符串” 。
14.2.4 在初始化期间分配常量属性
只要在初始化完成时将其设置为确定值,就可以在初始化期间的任何时刻为常量属性赋值。为常量属性分配值后,无法进一步修改。
注意
对于类实例,只能通过引入它的类在初始化期间修改常量属性。它不能被子类修改。
你可以修改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.)"
13.3 默认初始化
在Swift中,如果一个类没有提供任何初始化方法或继承任务初始化方法,则编译器会为其提供了一个默认初始化方法,该方法对于所有其属性提供缺省值。
此示例定义了一个名为的类ShoppingListItem,它封装了购物清单中商品的名称,数量和购买状态:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
由于ShoppingListItem的所有属性都具有默认值,并且因为它是没有超类的基类,因此ShoppingListItem自动获得一个默认初始化方法,并将其所有属性设置为其默认值。(该name属性是一个可选String属性,因此它会自动接收默认值nil,即使此值未写入代码中。)上面的示例使用ShoppingListItem该类的默认初始化程序创建具有初始化程序的类的新实例语法,写为ShoppingListItem(),并将此新实例分配给名为的变量item。
结构类型的成员初始化器
如果结构类型没有定义任何自己的自定义初始化方法,它们会自动接收成员初始值设定项。与默认初始化不同,即使结构存储的属性没有默认值,该结构也会接收成员初始值设定项。
成员初始化程序是初始化新结构实例的成员属性的简便方法。可以通过名称将新实例的属性的初始值传递给成员初始值设定项。
下面的例子定义了一个名为结构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)
13.4 值类型的初始化程序委派
初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委派,可避免跨多个初始化程序复制代码。
初始化程序委派的工作原理以及允许的委托形式的规则对于值类型和类类型是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委派过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。
对于值类型,self.init
在编写自己的自定义初始值设定项时,用于引用相同值类型的其他初始值设定项self.init
只能在初始化程序中调用。
请注意,如果为值类型定义自定义初始化方法,则你将无法再访问该类型的默认初始化方法。此约束可防止使用其中一个自动初始化程序的人意外绕过更复杂的初始化程序中提供的其他基本设置的情况。
以下示例定义了Rect
表示几何矩形的自定义结构。的例子需要两个称为支撑结构Size
和Point
,两者都提供的默认值0.0
对于所有其属性的:
1. struct Size {
2. var width = 0.0, height = 0.0
3. }
4. struct Point {
5. var x = 0.0, y = 0.0
6. }
你可以Rect
通过三种方式之一初始化下面的结构 - 使用其默认的零初始化origin
和size
属性值,提供特定的原点和大小,或者提供特定的中心点和大小。这些初始化选项由三个自定义初始值设定项表示,它们是Rect
结构定义的一部分:
1. struct Rect {
2. var origin = Point()
3. var size = Size()
4. init() {}
5. init(origin: Point, size: Size) {
6. self.origin = origin
7. self.size = size
8. }
9. init(center: Point, size: Size) {
10. let originX = center.x - (size.width / 2)
11. let originY = center.y - (size.height / 2)
12. self.init(origin: Point(x: originX, y: originY), size: size)
13. }
14. }
第一个Rect
初始化程序init()
在功能上与结构在没有自己的自定义初始值设定项时可以接收的默认初始化程序相同。这个初始化器有一个空体,由一对空花括号表示{}
。调用此初始化返回一个Rect
实例,其origin
和size
性能与的默认值都初始化并从他们的属性定义:Point(x: 0.0, y: 0.0)``Size(width: 0.0, height: 0.0)
1. let basicRect = Rect()
2. // basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)
第二个Rect
初始化init(origin:size:)
方法在功能上与结构在没有自己的自定义初始化方法时将收到的成员初始值设定项相同。此初始化程序只是将origin
和size
参数值分配给适当的存储属性:
1. let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
2. size: Size(width: 5.0, height: 5.0))
3. // 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:)
初始化程序,初始化程序将新的原点和大小值存储在适当的属性中:
1. let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
2. size: Size(width: 3.0, height: 3.0))
3. // centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
在init(center:size:)
初始化可能分配的新值origin
,并size
以相应的属性本身。但是,init(center:size:)
初始化程序利用已经提供了该功能的现有初始化程序更方便(并且意图更清晰)。
13.5 类继承和初始化
所有类的存储属性(包括该类从其超类继承的任何属性)都必须在初始化期间分配初始值。
Swift为类类型定义了两种初始值设定项,以帮助确保所有存储的属性都接收初始值。这些被称为指定的初始化器和便利初始化器。
13.5.1 指定的初始化器和便利初始化器
指定的初始值化器是类的主要初始值设定项。指定的初始化程序完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类的初始化过程。类往往只有很少的指定初始化器,并且一个类只有一个是很常见的。
每个类必须至少有一个指定的初始化程序
。在某些情况下,通过从超类继承一个或多个指定的初始值设定项来满足此要求。
便利初始化程序是次要的,支持类的初始化程序。你可以定义一个便捷初始化器,以便从与便捷初始化程序相同的类中调用指定的初始值设定项,并将某些指定的初始值设定项参数设置为默认值。你还可以定义一个便捷初始值设定项,以便为特定用例或输入值类型创建该类的实例。
如果您的类不需要,您不必提供方便的初始化程序。只要用便利初始化器可以节省时间或使类的初始化意图更清晰,才创建便利初始化器。
13.5.2 指定和便利初始化器的语法
类的指定初始值设定项的编写方式与值类型的简单初始值设定项相同:
init(parameters) {
statements
}
便捷初始化程序以相同的样式编写,但convenience修饰符放在init关键字之前,用空格分隔:
convenience init(parameters) {
statements
}
13.5.3 类类型的初始化程序委派
为了简化指定和便利初始化程序之间的关系,Swift对初始化程序之间的委托调用应用以下三个规则:
规则1
指定的初始化程序必须从其直接超类调用指定的初始化程序。
规则2
便捷初始化程序必须从同一个类调用另一个初始值设定项。
规则3
便捷初始化程序必须最终调用指定的初始化程序。
记住这个的一个简单方法是:
- 指定的初始值必须始终委派了。
- 便利的初始化必须始终委派跨越。
这些规则如下图所示:
这里,超类有一个指定的初始化器和两个便利初始化器。一个便利初始化器调用另一个便利初始化器,后者又调用单个指定的初始化器。这满足上面的规则2和3。超类本身不具有另一个超类,因此规则1不适用。
该图中的子类有两个指定的初始化器和一个便利初始化器。便捷初始化程序必须调用两个指定的初始化程序之一,因为它只能从同一个类中调用另一个初始化程序。这满足上面的规则2和3。两个指定的初始值设定项必须从超类中调用单个指定的初始值设定项,以满足上面的规则1。
注意
这些规则不会影响类的用户如何创建每个类的实例。上图中的任何初始化程序都可用于创建它们所属类的完全初始化的实例。规则仅影响您编写类初始值设定项的实现方式。
下图显示了四个类的更复杂的类层次结构。它说明了此层次结构中的指定初始值设定项如何作为类初始化的“漏斗”点,简化了链中类之间的相互关系:
../_images/initializerDelegation02_2x.png13.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如何查找假设子类和超类的初始化调用:
../_images/twoPhaseInitialization01_2x.png
在此示例中,初始化始于对子类上的便捷初始化程序的调用。此便捷初始化程序尚不能修改任何属性。它委托来自同一类的指定初始化程序。
根据安全检查1,指定的初始化程序确保所有子类的属性都有一个值。然后它在其超类上调用指定的初始化程序以继续初始化链。
超类的指定初始值设定项确保所有超类属性都具有值。没有其他超类要初始化,因此不需要进一步委派。
只要超类的所有属性都具有初始值,就会认为其内存已完全初始化,并且第1阶段已完成。
以下是阶段2查找相同初始化调用的方式:
../_images/twoPhaseInitialization02_2x.png
超类的指定初始化程序现在有机会进一步自定义实例(尽管它没有)。
一旦超类的指定初始化程序完成,子类的指定初始化程序就可以执行额外的自定义(尽管它也不需要)。
最后,一旦子类的指定初始化程序完成,最初调用的便捷初始化程序可以执行其他自定义。
13.5.5 初始化程序继承和覆盖
与Objective-C中的子类不同,Swift子类默认不继承其超类初始值设定项。Swift的方法可以防止超类中的简单初始化程序由更专业的子类继承,并用于创建未完全或正确初始化的子类的新实例。
注意
超类初始值化器在某些情况下是继承的,但只有在安全且适当的情况下才会继承。
如果希望自定义子类与其超类一起呈现一个或多个相同的初始值设定项,则可以在子类中提供这些初始值设定项的自定义实现。
当你编写与超类指定初始值化器匹配的子类初始化器时,实际上提供了对该指定初始化器的覆盖。因此,必须在子类的初始化程序定义之前编写修饰符——override
。即使你重写了自动提供的默认初始化程序,也是如此。
与重写的属性,方法或下标一样,override
修饰符的存在提示Swift检查超类是否具有要覆盖的匹配的指定初始化程序,并验证已覆盖初始化程序的参数已按预期指定。
注意
override
在覆盖超类指定的初始化程序时,总是编写修饰符,即使您的子类的初始化程序的实现是一个便利初始化程序。
相反,如果编写与超类便捷初始值化器匹配的子类初始值化器,则根据上面在类类型的初始化程序委派中描述的规则,永远不能由子类直接调用该超类便捷初始化器。因此,你的子类(严格来说)不提供超类初始化程序的覆盖。因此,override
在提供超类便捷初始化程序的匹配实现时,不要编写修饰符。
下面的示例定义了一个名为的基类Vehicle
。此基类声明一个名为的存储属性numberOfWheels
,其默认Int
值为0
。该numberOfWheels
属性由被调用的计算属性使用,description
以创建String
车辆特征的描述:
1. class Vehicle {
2. var numberOfWheels = 0
3. var description: String {
4. return "\(numberOfWheels) wheel(s)"
5. }
6. }
本Vehicle
类提供了其唯一的存储属性的默认值,并没有提供任何自定义初始化本身。因此,它会自动接收默认初始值设定项。默认初始化(如果有的话)始终是一类指定初始化,并且可以用来创建一个新的Vehicle
带有实例numberOfWheels
的0
:
1. let vehicle = Vehicle()
2. print("Vehicle: \(vehicle.description)")
3. // Vehicle: 0 wheel(s)
下一个示例定义了一个Vehicle
被调用的子类Bicycle
:
1. class Bicycle: Vehicle {
2. override init() {
3. super.init()
4. numberOfWheels = 2
5. }
6. }
该Bicycle
子类定义自定义指定初始化,init()
。此指定的初始值设定项匹配来自超类的指定初始值设定项Bicycle
,因此Bicycle
该初始值设定项的版本使用override
修饰符进行标记。
该init()
用于初始化Bicycle
通过调用开始super.init()
,这就要求默认初始化Bicycle
类的超类Vehicle
。这样可以确保在有机会修改属性之前numberOfWheels
初始化继承的属性。调用后,原始值将替换为新值。Vehicle``Bicycle``super.init()``numberOfWheels``2
如果您创建了一个实例Bicycle
,则可以调用其继承的description
计算属性以查看其numberOfWheels
属性的更新方式:
1. let bicycle = Bicycle()
2. print("Bicycle: \(bicycle.description)")
3. // Bicycle: 2 wheel(s)
如果子类初始化程序在初始化过程的阶段2中不执行自定义,并且超类具有零参数指定的初始化程序,则可以super.init()
在为所有子类的存储属性赋值之后省略对其的调用。
此示例定义了另一个子类Vehicle
,名为Hoverboard
。在其初始化程序中,Hoverboard
该类仅设置其color
属性。super.init()
此初始化程序不依赖于显式调用,而是依赖对其超类的初始化程序的隐式调用来完成该过程。
1. class Hoverboard: Vehicle {
2. var color: String
3. init(color: String) {
4. self.color = color
5. // super.init() implicitly called here
6. }
7. override var description: String {
8. return "\(super.description) in a beautiful \(color)"
9. }
10. }
一个实例Hoverboard
使用Vehicle
初始化程序提供的默认轮数。
1. let hoverboard = Hoverboard(color: "silver")
2. print("Hoverboard: \(hoverboard.description)")
3. // Hoverboard: 0 wheel(s) in a beautiful silver
注意
子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。
13.5.6 继承自动初始化器
如上所述,默认情况下,子类不会继承其超类初始值设定项。但是,如果满足某些条件,则会自动继承超类初始值设定项。实际上,这意味着您不需要在许多常见场景中编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承您的超类初始化程序。
假设您为在子类中引入的任何新属性提供默认值,则适用以下两个规则:
规则1
如果您的子类没有定义任何指定的初始值设定项,它会自动继承其所有超类指定的初始值设定项。
规则2
如果您的子类提供了所有超类指定初始化程序的实现 - 通过按照规则1继承它们,或者通过提供自定义实现作为其定义的一部分 - 那么它会自动继承所有超类便捷初始化程序。
即使您的子类添加了更多便利初始值设定项,这些规则也适用。
注意
子类可以将超类指定的初始化程序实现为子类便捷初始化程序,作为满足规则2的一部分。
行动中的指定和便利初始化器
以下示例显示了指定的初始化程序,便捷初始化程序和自动初始化程序继承。这个例子定义了三类所谓的层次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)``Food``Food``Food``init(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
班有一个单一的指定初始化,,它可以用来填充的所有新的属性的实例。此初始化程序首先将传递的参数赋值给属性,该属性是唯一引入的新属性。执行此操作后,初始化程序将委托给该类的初始化程序。该过程满足上述两阶段初始化的安全检查1 。init(name: String, quantity: Int)``RecipeIngredient``quantity``quantity``RecipeIngredient``init(name: String)``Food
RecipeIngredient
还定义了一个便利初始化程序,它仅用于按名称创建实例。此便捷初始化程序假定在没有显式数量的情况下创建的任何实例的数量。此便捷初始化程序的定义使实例创建更快捷,更方便,并在创建多个单数量实例时避免代码重复。这个方便的初始化程序只需委托给类的指定初始化程序,传入一个值。init(name: String)``RecipeIngredient``1``RecipeIngredient``RecipeIngredient``RecipeIngredient``quantity``1
提供的便利初始化程序采用与指定初始化程序相同的参数。由于此便捷初始值设定项会覆盖其超类中的指定初始值设定项,因此必须使用修饰符进行标记(如初始化程序继承和覆盖中所述)。init(name: String)``RecipeIngredient``init(name: String)
Food``override
即使RecipeIngredient
提供初始化器作为便利初始化器,仍然提供了其所有超类的指定初始化器的实现。因此,自动继承其所有超类的便利初始化器。init(name: String)``RecipeIngredient``RecipeIngredient
在这个例子中,RecipeIngredient
is 的超类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
自动从其超类继承所有指定和便利初始化器。
下图显示了所有三个类的整体初始化链:
../_images/initializersExample03_2x.png您可以使用所有三个继承的初始值设定项来创建新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"
可用的初始化器
定义初始化可能失败的类,结构或枚举有时很有用。此失败可能由无效的初始化参数值,缺少必需的外部资源或阻止初始化成功的某些其他条件触发。
要处理可能失败的初始化条件,请将一个或多个可用的初始值设定项定义为类,结构或枚举定义的一部分。您可以通过在init
关键字(init?
)后面放置一个问号来编写可用的初始化程序。
注意
您无法使用相同的参数类型和名称定义可用的和不可用的初始值设定项。
可用的初始值设定项创建其初始化类型的可选值。您可以在一个可用的初始化程序中编写,以指示可以触发初始化失败的点。return nil
注意
严格地说,初始值设定项不返回值。相反,它们的作用是确保self
在初始化结束时完全正确地初始化。虽然您编写以触发初始化失败,但您不使用该关键字来指示初始化成功。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
。此初始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"
如果将空字符串值传递给failable初始化程序的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
属性的值是不合适的。要对此限制建模,如果找到空字符串,则可用的初始化程序会触发初始化失败。
枚举的可用初始化程序
您可以使用可用的初始化程序根据一个或多个参数选择适当的枚举大小写。如果提供的参数与适当的枚举情况不匹配,则初始化程序可能会失败。
下面的例子定义称为枚举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."
具有原始值的枚举的可用初始值设定项
具有原始值的枚举会自动接收一个可用的初始化程序,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."
初始化失败的传播
类,结构或枚举的可用初始化程序可以从同一类,结构或枚举委托给另一个可用的初始化程序。类似地,子类failable初始化程序可以委托一个超类可用的初始化程序。
在任何一种情况下,如果您委托另一个导致初始化失败的初始化程序,整个初始化过程将立即失败,并且不会执行进一步的初始化代码。
注意
可用的初始化程序也可以委托给不可用的初始化程序。如果您需要将潜在的故障状态添加到现有的初始化过程中,否则请使用此方法,否则该过程将失败。
下面的示例定义了一个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
或更多的实例,则初始化成功:
- if let twoSocks = CartItem(name: "sock", quantity: 2) {
- print("Item: (twoSocks.name), quantity: (twoSocks.quantity)")
- }
- // Prints "Item: sock, quantity: 2"
如果尝试创建值为的CartItem
实例,则初始化程序会导致初始化失败:quantity``0``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
使用空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"
覆盖可用的初始化程序
您可以在子类中覆盖超类可用的初始化程序,就像任何其他初始化程序一样。或者,您可以使用子类nonfailable初始化程序覆盖超类可用的初始化程序。这使您可以定义初始化不会失败的子类,即使允许超类的初始化失败也是如此。
请注意,如果使用不可用的子类初始值设定项覆盖可用的超类初始值设定项,则委派超类初始值设定项的唯一方法是强制解包可用的超类初始值设定项的结果。
注意
您可以使用不可用的初始化程序覆盖可用的初始化程序,但不能反过来。
下面的示例定义了一个名为的类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
不可init?(name:)
用的init(name:)
初始化程序覆盖其超类的可用初始化程序。因为AutomaticallyNamedDocument
以与其超类不同的方式处理空字符串大小写,所以它的初始化程序不需要失败,因此它提供了初始化程序的不可用版本。
您可以在初始化程序中使用强制解包来从超类调用可用的初始化程序,作为子类的不可用初始化程序的实现的一部分。例如,UntitledDocument
下面的子类总是被命名"[Untitled]"
,并且它init(name:)
在初始化期间使用来自其超类的可用初始化器。
- class UntitledDocument: Document {
- override init() {
- super.init(name: "[Untitled]")!
- }
- }
在这种情况下,如果init(name:)
使用空字符串作为名称调用超类的初始化程序,则强制解包操作将导致运行时错误。但是,因为它是使用字符串常量调用的,所以您可以看到初始化程序不会失败,因此在这种情况下不会发生运行时错误。
初学者!可用的初始化程序
您通常会定义一个可用的初始值设定项,通过在init
关键字(init?
)后面放置一个问号来创建相应类型的可选实例。或者,您可以定义一个可用的初始值设定项,用于创建相应类型的隐式解包的可选实例。通过在init
关键字(init!
)后面放置一个感叹号而不是问号来做到这一点。
您可以从委托init?
到init!
,反之亦然,你可以覆盖init?
与init!
反之亦然。您也可以委托init
自己init!
,尽管这样做会在init!
初始化程序导致初始化失败时触发断言。
必需的初始化程序
required
在类初始化程序的定义之前编写修饰符,以指示该类的每个子类都必须实现该初始化程序:
- class SomeClass {
- required init() {
- // initializer implementation goes here
- }
- }
您还必须required
在所需初始化程序的每个子类实现之前编写修饰符,以指示初始化程序要求适用于链中的其他子类。override
覆盖所需的指定初始化程序时,不要编写修饰符:
- class SomeSubclass: SomeClass {
- required init() {
- // subclass implementation of the required initializer goes here
- }
- }
注意
如果可以使用继承的初始化程序满足要求,则不必提供所需初始化程序的显式实现。
使用闭包或函数设置默认属性值
如果存储属性的默认值需要某些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,将调用闭包或函数,并将其返回值指定为属性的默认值。
这些类型的闭包或函数通常会创建与属性相同类型的临时值,定制该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。
这是一个关于如何使用闭包来提供默认属性值的骨架轮廓:
- 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"