案例代码下载
初始化
初始化是准备要使用的类,结构或枚举的实例的过程。此过程涉及为该实例上的每个存储属性设置初始值,并在新实例准备好使用之前执行所需的任何其他设置或初始化。
可以通过定义初始化器实现这个初始化,这就像特殊的方法,可以被调用来创建特定类型的新实例。与Objective-C初始值设定项不同,Swift初始值设定项不返回值。它们的主要作用是确保在第一次使用类型的新实例之前正确初始化它们。
类类型的实例也可以实现deinitializer,它在释放该类的实例之前执行任何自定义清理。有关deinitializer的详细信息,请参阅Deinitialization。
设置存储属性的初始值
在创建类或结构的实例时,类和结构必须将其所有存储的属性设置为适当的初始值。存储的属性不能保留不确定的状态。
可以在初始化程序中为存储的属性设置初始值,也可以通过将默认属性值指定为属性定义的一部分来设置初始值。以下各节介绍了这些操作。
注意: 将默认值分配给存储属性或在初始化程序中设置其初始值时,将直接设置该属性的值,而不调用任何属性观察者。
初始化器
调用初始化器创建特定类型的新实例。在最简单的形式中,初始化器就像一个没有参数的实例方法,使用init关键字编写:
init() {
// 在这里执行初始化
}
下面的示例定义了一个新结构,Fahrenheit用于存储以华氏温标表示的温度。Fahrenheit结构有一个存储属性temperature,其类型为Double:
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
let f = Fahrenheit()
print(f.temperature)
该结构定义了一个init没有参数的单个初始化器,它以一个值32.0(华氏度的水的凝固点)初始化存储的温度。
默认属性值
可以在初始化程序中设置存储属性的初始值,如上所示。或者,将默认属性值指定为属性声明的一部分。通过在定义属性时为其指定初始值,可以指定默认属性值。
注意: 如果属性始终采用相同的初始值,请提供默认值,而不是在初始化设定项中设置值。最终结果是相同的,但默认值将属性的初始化与其声明更紧密地联系在一起。它使初始化程序更短,更清晰,并能够从其默认值推断属性的类型。还可以更轻松地利用默认初始值设定项和初始化程序继承,如本章后面所述。
Fahrenheit可以通过在声明属性的位置为其属性temperature提供默认值,以更简单的形式编写上面的结构:
struct Fahrenheit {
var temperature = 32.0
}
自定义初始化
可以使用输入参数和可选属性类型自定义初始化过程,或者在初始化期间分配常量属性,如以下部分所述。
初始化参数
可以提供初始化参数作为初始化程序定义的一部分,以定义自定义初始化过程的值的类型和名称。初始化参数具有与函数和方法参数相同的功能和语法。
以下示例定义了一个名为的结构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)
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
第一个初始化程序有一个初始化参数,参数标签为fromFahrenheit,参数名称为fahrenheit。第二个初始化程序有一个初始化参数,参数标签为fromKelvin,参数名称为kelvin。两个初始值设定项都将其单个参数转换为相应的Celsius值,并将此值存储在名为的temperatureInCelsius属性中。
参数名称和参数标签
与函数和方法参数一样,初始化参数既可以具有在初始化程序体内使用的参数名称,也可以具有在调用初始化程序时使用的参数标签。
但是,初始化程序在其括号之前没有函数和方法的函数标识名称。因此,初始化程序参数的名称和类型在识别应该调用哪个初始化程序时起着特别重要的作用。因此,如果不提供初始化程序,Swift会为初始化程序中的每个参数提供自动参数标签。
下面的例子定义了一个名为结构Color,具有三个恒定属性叫做red,green,和blue。这些属性在两者之间存储一个0.0到1.0的值用于指示颜色中红色,绿色和蓝色的数量。
Color为初始化程序提供了三个适当命名的红色,绿色和蓝色组件的Double类型参数。Color还提供了具有单个white参数的第二个初始化器,用于为所有三个颜色组件提供相同的值。
struct Color {
var 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)
没有参数标签的初始化参数
如果您不想对初始化参数使用参数标签,请为该参数编写下划线(_)而不是显式参数标签以覆盖默认行为。
以下是上面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)
初始化调用程序Celsius(37.0)其意图是明确的,无需参数标签。因此,编写init(_ celsius: Double)Double此初始化程序是合适的,以便可以通过提供未命名的值来调用它。
可选的属性类型
如果自定义类型具有逻辑上允许具有“无值”的存储属性 - 可能因为在初始化期间无法设置其值,或者因为在稍后的某个时间点允许它具有“无值” - 请使用可选类型。可选类型的属性将自动初始化为值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()
在询问调查问题之前,无法知道对调查问题的响应,因此response声明属性的类型为String?“可选String”。当SurveyQuestion初始化新实例时,会自动为其分配默认值nil,意味着“还没有字符串” 。
在初始化期间分配常量属性
只要在初始化完成时将其设置为确定值,就可以在初始化期间的任何时刻为常量属性赋值。为常量属性分配值后,无法进一步修改。
注意: 对于类实例,只能通过引入它的类在初始化期间修改常量属性。它不能被子类修改。
可以修改上面的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: "Do you like teets?")
beetsQuestion.ask()
beetsQuestion.response = "I also like beets. (But not with cheese.)"
默认初始化程序
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。通过指定默认值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)
值类型的初始化程序委派
初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委派,可避免跨多个初始化程序复制代码。
初始化程序委派的工作原理以及允许的委派形式的规则对于值类型和类类型是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委派过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。但是,类可以从其他类继承,如继承中所述。这意味着类具有额外的职责,以确保在初始化期间为其继承的所有存储属性分配合适的值。下面的类继承和初始化中描述了这些职责。
对于值类型,在编写自定义初始化时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.0
let originY = center.y - size.height/2.0
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()
第二个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))
第三个Rect初始化init(center:size:)稍微复杂一些。它首先根据center点和size值计算适当的原点。然后它调用(或委托)init(origin:size:)初始化程序,初始化程序将新的原点和大小值存储在适当的属性中:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))
在init(center:size:)初始化可能分配的新的origin和size值给自身相应的属性。但是,init(center:size:)初始化程序利用已经提供了该功能的现有初始化程序更方便(并且意图更清晰)。
注意: 有关在不自定义init()和init(origin:size:)初始化程序的情况下编写此示例的替代方法,请参阅扩展。
类继承和初始化
所有类的存储属性(包括该类从其超类继承的任何属性)都必须在初始化期间分配初始值。
Swift为类类型定义了两种初始化,以帮助确保所有存储的属性都接收初始值。这些被称为指定的初始化器和便利初始化器。
指定的初始化器和便利初始化器
指定的初始化是类的主要初始化。指定的初始化程序完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类链的初始化过程。
类往往只有很少的指定初始化器,并且一个类只有一个是很常见的。指定的初始化器是初始发生的起始点,初始化过程通过该点继续父类链。
每个类必须至少有一个指定的初始化程序。在某些情况下,通过从超类继承一个或多个指定的初始化来满足此要求,如下面的自动初始化程序继承中所述。
便利初始化程序是次要的,支持类的初始化程序。可以定义一个便捷初始值设定项,以便从与便捷初始化程序相同的类中调用指定的初始值设定项,并将某些指定的初始值设定项参数设置为默认值。还可以定义一个便捷初始值设定项,以便为特定用例或输入值类型创建该类的实例。
如果类不需要,则不必提供方便的初始化程序。只要通用初始化模式的快捷方式可以节省时间或使类的初始化意图更清晰,就可以创建便利初始化程序。
指定和便利初始化器的语法
类的指定初始化器的编写方式与值类型初始化器相同:
init(parameters) {
statements
}
便捷初始化程序以相同的样式编写,但convenience修饰符放在init关键字之前,用空格分隔:
convenience init(parameters) {
statements
}
类类型的初始化程序委派
为了简化指定和便利初始化程序之间的关系,Swift对初始化程序之间的委托调用应用以下三个规则:
- 指定的初始化程序必须从其直接超类调用指定的初始化程序。
- 便捷初始化程序必须从同一个类调用另初始化程序。
- 便捷初始化程序必须最终调用指定的初始化程序。
记住这个的一个简单方法是:
- 指定的初始化必须始终向上级委派。
- 便利的初始化必须始终向同级委派。
这些规则如下图所示:
这里,超类有一个指定的初始化器和两个便利初始化器。一个便利初始化器调用另一个便利初始化器,后者又调用单个指定的初始化器。这满足上面的规则2和3。超类本身不具有另一个超类,因此规则1不适用。
该图中的子类有两个指定的初始化器和一个便利初始化器。便捷初始化程序必须调用两个指定的初始化程序之一,因为它只能从同一个类中调用另一个初始化程序。这满足上面的规则2和3。两个指定的初始值设定项必须从超类中调用单个指定的初始值设定项,以满足上面的规则1。
注意: 这些规则不会影响类的用户如何创建每个类的实例。上图中的任何初始化程序都可用于创建它们所属类的完全初始化的实例。规则仅影响编写类初始值设定项的实现方式。
下图显示了四个类的更复杂的类层次结构。它说明了此层次结构中的指定初始化如何作为类初始化的起始点,简化了链中类之间的相互关系:
两步初始化
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查找相同初始化调用的方式:
超类的指定初始化程序现在有机会进一步自定义实例(尽管它不必须)。
一旦超类的指定初始化程序完成,子类的指定初始化程序就可以执行额外的自定义(尽管如此,它不必须)。
最后,一旦子类的指定初始化程序完成,最初调用的便捷初始化程序可以执行其他自定义。
初始化程序继承和覆盖
与Objective-C中的子类不同,Swift子类默认不继承其超类初始化。Swift的方法可以防止超类中的简单初始化程序被更专业的子类继承,并用于创建未完全或正确初始化的子类的新实例。
注意: 超类初始化在某些情况下是继承的,但只有在安全且适当的情况下才会继承。有关更多信息,请参阅下面的自动初始化程序继承。
如果希望自定义子类与其超类一起呈现一个或多个相同的初始化,则可以在子类中提供这些初始化的自定义实现。
当编写与超类指定初始化匹配的子类初始化时,实际上提供了对该指定初始化的覆盖。因此,必须在子类的初始化程序定义之前编写override修饰符。即使重写了自动提供的默认初始化程序,也是如此,如Default Defaultizers中所述。
与重写的属性,方法或下标一样,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.description)
下一个示例定义了一个Vehicle的子类Bicycle:
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
Bicycle子类定义自定义指定初始化,init()。此指定的初始匹配来自Bicycle的超类指定初始化,因此Bicycle该初始的版本使用override修饰符进行标记。
init()用于初始化Bicycle通过调用super.init()开始,这就要求默认初始化Bicycle类的超类Vehicle。这样可以确保在有机会修改Bicycle属性之前初始化继承Vehicle的numberOfWheels属性。调用super.init()后,numberOfWheels原始值将替换为新值2。
如果创建了一个实例Bicycle,则可以调用其继承的description计算属性以查看其numberOfWheels属性的更新方式:
let bicycle = Bicycle()
print(bicycle.description)
如果子类初始化程序在初始化过程的阶段2中不执行自定义,并且超类具有零参数的指定初始化程序,则可在将值分配给所有子类的存储属性之后省略对以super.init()的调用。
此示例定义了另一个Vehicle子类,名为Hoverboard。在其初始化程序中,Hoverboard该类仅设置其color属性。此初始化程序不依赖于super.init()显式调用,而是依赖对其超类的初始化程序的隐式调用来完成该过程。
class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}
一个Hoverboard实例使用Vehicle初始化程序提供的默认轮数。
let hoverboard = Hoverboard(color: "silver")
print(hoverboard.description)
注意: 子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。
自动初始化程序继承
如上所述,默认情况下,子类不会继承其超类初始化。但是,如果满足某些条件,则会自动继承超类初始化。实际上,这意味着不需要在许多常见场景中编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承超类初始化程序。
假设在子类中引入的任何新属性提供默认值,则适用以下两个规则:
规则1
如果子类没有定义任何指定的初始化,它会自动继承其所有超类指定的初始化。
规则2
如果子类提供了所有超类指定初始化程序的实现 - 要么是按照规则1继承它们,或者通过提供自定义实现作为其定义的一部分 - 那么它会自动继承所有超类便捷初始化程序。
即使子类添加了更多便利初始化,这些规则也适用。
注意: 子类可以将超类指定的初始化程序实现为子类便捷初始化程序,作为满足规则2的一部分。
行动中的指定和便利初始化器
以下示例显示了指定的初始化程序,便捷初始化程序和自动初始化程序继承。这个例子定义了三个类层次分别是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类没有默认的成员初始化程序,因此Food该类提供了一个指定的初始化程序,它接受一个名为的参数name。此初始化可用于创建Food具有特定名称的新实例:
let food = Food(name: "Bacon")
来自Food类的初始化程序init(name: String)作为指定的初始化程序提供,因为它确保完全初始化新Food实例的所有存储属性。Food类没有超类,所以init(name: String)初始化并不需要调用super.init()来完成初始化。
该Food类还提供了一个方便的初始化init()不带参数。该init()初始化通过使用name值为[Unnamed]委派给Food类的init(name: String)为新Food实例提供了默认的占位符名称:
let mysteryMeat = Food()
层次结构中的第二个类是Food的子类RecipeIngredient。RecipeIngredient类构建烹饪技法。它引入了一个Int类型名为quantity(除了它继承的属性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实例的所有新的属性。此初始化程序首先将传递参数赋值给quantity属性,quantity属性是RecipeIngredient唯一引入的新属性。执行此操作后,初始化程序将委托给Food类的初始化程序init(name: String)。该过程满足上述两阶段初始化的安全检查1 。
RecipeIngredient还定义了一个便利初始化程序。init(name: String),它仅使用名称创建RecipeIngredient实例。此便捷初始化程序假定在没有显式quantity的情况下创建的任何RecipeIngredient实例的quantity为1。此便捷初始化程序的定义使RecipeIngredient实例创建更快捷,更方便,并在创建多个实例时避免代码重复。这个方便的初始化程序只需委托给RecipeIngredient类的指定初始化程序,传入一个quantity值为1。
RecipeIngredientinit提供的便利初始化程序init(name: String)采用与Food的指定初始化程序init(name: String)相同的参数。由于此便捷初始化会覆盖其超类中的指定初始化,因此必须使用override修饰符进行标记(如初始化程序继承和覆盖中所述)。
即使RecipeIngredient提供初始化器init(name: String)作为便利初始化器,RecipeIngredient仍然提供了其所有超类的指定初始化器的实现。因此,RecipeIngredient自动继承其所有超类的便利初始化器。
在这个例子中,RecipeIngredientis 的超类Food,它有一个便利的初始化程序init()。此初始化程序被RecipeIngredient继承。init()函数的继承版本与Food版本完全相同,只是它委托给RecipeIngredient版本而不是Food版本的init(name: String)。
所有这三个初始化都可用于创建新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)
}
这里,从包含三个新ShoppingListItem实例的数组文字创建一个新的breakfastList数组。推断出数组的类型[ShoppingListItem]。创建数组后,数组开头的ShoppingListItem名称"[Unnamed]"更改为"Orange juice",并将其标记为已购买。打印数组中每个项目的描述表明它们的默认状态已按预期设置。
可失败的初始化程序
定义初始化可能失败的类,结构或枚举有时很有用。此失败可能由无效的初始化参数值,缺少必需的外部资源或阻止初始化成功的某些其他条件触发。
要处理可能失败的初始化条件,定义一个或多个可能失败的初始化作为为类,结构或枚举定义的一部分。可以通过在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)")
}
let valueChanged = Int(exactly: pi)
if valueChanged == nil {
print("\(pi) conversion to Int does not maintain value")
}
下面的示例定义了一个名为Animal的结构,其中包含一个String类型的species常量属性。该Animal结构还定义了一个可失败的初始化程序,其中包含一个名为species的参数。此初始化检查传递给初始化的species值是否为空字符串。如果发现是空字符串,则触发初始化失败。否则,设置species属性的值,初始化成功:
struct Animal {
var species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
可以使用此可失败的初始化程序尝试初始化新Animal实例并检查初始化是否成功:
let someCreature = Animal(species: "Giraffe")
if let giraffe = someCreature {
print("An animal was initialized with a species of \(giraffe.species)")
}
如果将空字符串值传递给failable初始化程序的species参数,则初始化程序会触发初始化失败:
let anonymousCreature = Animal(species: "")
if anonymousCreature == nil {
print("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.")
}
let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("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.")
}
let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
print("This is not a defined temperature unit, so initialization failed.")
}
初始化失败的传播
类,结构或枚举的可失败初始化程序可以从同一类,结构或枚举委托给另一个可失败初始化程序。类似地,子类可失败初始化程序可以委托一个超类可失败的初始化程序。
在任何一种情况下,如果委托另一个导致初始化失败的初始化程序,整个初始化过程将立即失败,并且不会执行进一步的初始化代码。
注意: 可失败的初始化程序也可以委托给不可失败的初始化程序。需要将潜在的故障状态添加到现有的初始化过程中,否则使用此方法,该过程将失败。
下面的示例定义了一个Product的子类CartItem。该CartItem类构建在线购物车的商品。CartItem引入了一个存储常量属性quantity,并确保此属性的值至少为1:
class Product {
var name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
var 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)")
}
如果尝试创建quantity值为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")
}
同样,如果尝试使用空name值创建CartItem实例,则超类Product初始化程序会导致初始化失败:
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product")
}
覆盖可失败的初始化程序
可以在子类中覆盖超类可失败的初始化程序,就像任何其他初始化程序一样。或者,可以使用子类nonfailable初始化程序覆盖超类可失败的初始化程序。这使得可以定义初始化不会失败的子类,即使允许超类的初始化失败也是如此。
请注意,如果使用不可失败的子类初始化覆盖可失败的超类初始化,则委派超类初始值设定项的唯一方法是强制解包初始化的超类初始化的结果。
注意: 可以使用不可失败的初始化程序覆盖可失败的初始化程序,但不能反过来。
下面的示例定义了一个名为Document的类。此类对可以使用非空字符串值或nil但不能为空字符串的name属性进行初始化的文档进行构建:
class Document {
var name: String?
init() {}
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
下一个示例定义了Document的子类AutomaticallyNamedDocument。AutomaticallyNamedDocument子类覆盖由Document引入的指定初始化。如果实例初始化时没有名称,或者如果将空字符串传递给init(name:)初始化器,这些覆盖确保AutomaticallyNamedDocument实例的name具有初始值"[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?(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() {
// 在这里实现
}
}
还必须在必需的初始化程序的每个子类实现之前编写required修饰符,以指示初始化程序要求适用于链中的其他子类。override覆盖必需的指定初始化程序时,不要编写修饰符:
class SomeSubclass: SomeClass {
required init() {
// 在这里实现子类必须的初始化
}
}
注意: 如果可以使用继承的初始化程序满足要求,则不必提供必须的初始化程序。
使用闭包或函数设置默认属性值
如果存储属性的默认值需要某些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,将调用闭包或函数,并将其返回值指定为属性的默认值。
这些类型的闭包或函数通常会创建与属性相同类型的临时值,定制该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。
这是一个关于如何使用闭包来提供默认属性值的轮廓:
class SomeClass {
let someProperty: SomeType = {
// 使用闭包创建默认值
// someValue必须是SomeType类型
return someValue
}()
}
请注意,闭包的结束大括号后面是一对空括号。这告诉Swift立即执行闭包。如果省略这些括号,则尝试将闭包本身分配给属性,而不是闭包的返回值。
注意: 如果使用闭包来初始化属性,请记住在执行闭包时尚未初始化实例的其余部分。这意味着无法从闭包中访问任何其他属性值,即使这些属性具有默认值也是如此。也不能使用隐式self属性,也不能调用任何实例的方法。
下面的例子定义了一个名为Chessboard的结构,它为国际象棋游戏模型。国际象棋是在8 x 8的棋盘上进行的,有黑色和白色的交替方块。
为了表示这个游戏板,Chessboard结构有一个名为boardColors的属性,它是一个包含64个Bool值的数组。true数组中的值表示黑色方块,值false表示白色方块。数组中的第一项表示板上的左上方,数组中的最后一项表示板上的右下方。
boardColors数组使用闭包设置它的初始化颜色值:
struct Chessboard {
var boardColors: [Bool] = {
var temporaryBoard = [Bool]()
var isBlack = false
for i in 1...8 {
for j in 1...8 {
temporaryBoard.append(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))
print(board.squareIsBlackAt(row: 7, column: 7))