Swift5.4 语言指南(十六) 初始化
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/)
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9739365.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
初始化是准备使用的类,结构或枚举实例的过程。此过程涉及为该实例上的每个存储属性设置初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。
您可以通过定义初始值设定项来实现此初始化过程,初始值设定项类似于可以调用以创建特定类型新实例的特殊方法。与Objective-C初始值设定项不同,Swift初始值设定项不会返回值。它们的主要作用是确保在首次使用类型之前,正确初始化类型的新实例。
类类型的实例还可以实现一个deinitializer,该方法将在释放该类的实例之前执行任何自定义清除。有关反初始化程序的更多信息,请参见反初始化。
设置存储属性的初始值
类和结构必须在创建该类或结构的实例时将其所有存储的属性设置为适当的初始值。存储的属性不能处于不确定状态。
您可以在初始化程序中为存储的属性设置初始值,或通过将默认属性值分配为属性定义的一部分来设置初始值。以下各节介绍了这些操作。
笔记
当您为存储的属性分配默认值,或在初始化程序中设置其初始值时,将直接设置该属性的值,而无需调用任何属性观察器。
初始化器
调用初始化程序以创建特定类型的新实例。最简单的形式是,初始化程序就像没有参数的实例方法,使用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
(水的冰点,以华氏度为单位)来初始化存储的温度。
默认属性值
您可以从初始化程序中设置存储属性的初始值,如上所示。或者,指定默认属性值作为属性声明的一部分。您可以通过在定义属性时为其分配初始值来指定默认属性值。
笔记
如果属性始终使用相同的初始值,请提供默认值,而不要在初始化程序中设置值。最终结果是相同的,但是默认值将属性的初始化与声明更紧密地联系在一起。它使初始化程序更短,更清晰,并使您能够从其默认值推断属性的类型。默认值还使您更容易利用默认初始化程序和初始化程序继承,如本章稍后所述。
您可以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)
- // boilingPointOfWater.temperatureInCelsius is 100.0
- let freezingPointOfWater = Celsius(fromKelvin: 273.15)
- // freezingPointOfWater.temperatureInCelsius is 0.0
第一个初始化程序具有单个初始化参数,其参数标签为fromFahrenheit
,参数名称为fahrenheit
。第二个初始化程序具有一个初始化参数,其参数标签为fromKelvin
,参数名称为kelvin
。两个初始化程序都将其单个参数转换为相应的摄氏度值,并将此值存储在名为的属性中temperatureInCelsius
。
参数名称和参数标签
与函数和方法参数一样,初始化参数可以具有在初始化程序的主体内使用的参数名称和在调用初始化程序时使用的参数标签。
但是,初始化函数在函数前没有括号,以函数和方法的方式标识函数名。因此,初始化器参数的名称和类型在确定应调用哪个初始化器中起着特别重要的作用。因此,如果您不提供初始化方法,则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
不带参数标签的初始化参数
如果不想为初始化参数使用参数标签,请为该参数写下划线(_
)而不是显式参数标签,以覆盖默认行为。
这是上述“初始化参数”中Celsius
示例的扩展版本,并带有一个附加的初始化程序,该初始化程序可根据已经在摄氏度范围内的值创建一个新实例:Celsius
Double
- 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
可选属性类型
如果您的自定义类型具有在逻辑上允许“无值”的存储属性(可能是因为在初始化过程中无法设置其值,或者是因为稍后允许使用“无值”),请使用可选类型。可选类型的属性将使用值自动初始化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?
或“ optional String
”类型声明。初始化nil
新实例时,SurveyQuestion
会自动为其指定默认值,表示“没有字符串” 。
在初始化期间分配常量属性
您可以在初始化期间的任何时候为常量属性分配一个值,只要在初始化完成时将其设置为确定的值即可。为常量属性分配值后,就无法再对其进行修改。
笔记
对于类实例,只能在引入常量的类的初始化期间对其进行修改。子类不能修改它。
您可以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.)"
默认初始化器
迅速提供了一个默认初始值对于所有其属性提供缺省值,并且不提供至少一个初始值设定本身的任何结构或类。默认初始化程序仅创建一个新实例,并将其所有属性设置为其默认值。
此示例定义了一个名为的类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)
调用成员初始化程序时,可以忽略具有默认值的任何属性的值。在上面的示例中,该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"
值类型的初始化程序委托
初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委托,可避免在多个初始化程序之间重复代码。
对于值类型和类类型,初始化程序委派的工作方式以及允许哪种形式的委派的规则是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委托过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。但是,类可以从其他类继承,如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
实例,该实例的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:)
器稍微复杂一些。首先根据一个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:)
初始化可能分配的新值origin
,并size
以相应的属性本身。但是,对于init(center:size:)
初始化程序而言,利用已经提供了该功能的现有初始化程序更方便(意图更清晰)。
笔记
有关无需自行定义init()
和init(origin:size:)
初始化程序的另一示例编写方式,请参见Extensions。
类继承和初始化
在初始化期间,必须为类的所有存储属性(包括该类从其超类继承的所有属性)分配一个初始值。
Swift为类类型定义了两种初始化器,以帮助确保所有存储的属性都接收初始值。这些被称为指定的初始值设定项和便利性初始值设定项。
指定的初始化程序和便利性初始化程序
指定的初始值设定项是类的主要初始值设定项。指定的初始化程序将完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类链中的初始化过程。
类往往只有很少的指定初始化器,而一个类只有一个很常见。指定的初始化程序是“漏斗”点,通过该“漏斗”点进行初始化,并通过该“漏斗”点继续超类链中的初始化过程。
每个类必须至少有一个指定的初始化程序。在某些情况下,可以通过从超类继承一个或多个指定的初始化程序来满足此要求,如下面的“自动初始化程序继承”中所述。
便利的初始值设定项是辅助的,支持类的初始值设定项。您可以定义一个便捷初始化程序,以从与便捷初始化程序相同的类中调用一个指定初始化程序,并将某些指定初始值设定项的参数设置为默认值。您还可以定义一个便捷初始化程序,以针对特定用例或输入值类型创建该类的实例。
如果您的班级不需要便利初始化器,则不必提供它们。只要通向通用初始化模式的快捷方式可以节省时间或使类的初始化更清晰,就可以创建方便的初始化器。
指定和便捷初始化程序的语法
指定的类初始值设定项的编写方式与值类型的简单初始值设定项的编写方式相同:
- init(parameters) {
- statements
- }
便捷初始化程序以相同的样式编写,但convenience
修饰符放在init
关键字之前,并用空格分隔:
- convenience init(parameters) {
- statements
- }
类类型的初始化程序委托
为了简化指定的初始化器和便捷的初始化器之间的关系,Swift将以下三个规则应用于初始化器之间的委托调用:
- 规则1
- 指定的初始值设定项必须从其直接超类调用指定的初始值设定项。
- 规则二
- 便捷初始化程序必须从同一类调用另一个初始化程序。
- 规则三
- 便利初始化程序必须最终调用指定的初始化程序。
记住这一点的一种简单方法是:
- 指定的初始值必须始终委派了。
- 便利的初始化必须始终委派跨越。
这些规则如下图所示:
在这里,超类具有一个指定的初始值设定项和两个便利的初始值设定项。一个便利初始化程序调用另一个便利初始化程序,后者又调用单个指定的初始化程序。这从上方满足规则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 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
带有的numberOfWheels
of的新实例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
笔记
子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。
自动初始化程序继承
如上所述,默认情况下,子类不继承其超类初始化器。但是,如果满足某些条件,则会自动继承超类初始化器。实际上,这意味着您无需在许多常见情况下编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承超类初始化程序。
假设为子类中引入的任何新属性提供默认值,则适用以下两个规则:
- 规则1
- 如果您的子类没有定义任何指定的初始值设定项,它将自动继承其所有超类指定的初始值设定项。
- 规则二
- 如果您的子类提供了其所有超类指定初始化器的实现(通过按规则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
提供的便捷初始化程序采用与中指定的初始化程序相同的参数。因为此便利初始化程序会覆盖其父类中的指定初始化程序,所以必须使用修饰符对其进行标记(如Initializer Inheritance和Overriding中所述)。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
自动从其超类继承所有指定的初始化和便捷初始化程序。
下图显示了所有三个类的整体初始化程序链:
您可以使用所有继承的三个初始化器来创建一个新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 doesn't maintain value")
- }
- // Prints "3.14159 conversion to Int doesn't 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 couldn't be initialized")
- }
- // Prints "The anonymous creature couldn't 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 isn't a defined temperature unit, so initialization failed.")
- }
- // Prints "This isn't a defined temperature unit, so initialization failed."
带有原始值的枚举失败的初始化程序
带有原始值的枚举会自动接收一个失败的初始化器,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 isn't a defined temperature unit, so initialization failed.")
- }
- // Prints "This isn't a defined temperature unit, so initialization failed."
初始化失败的传播
一个类,结构或枚举的故障初始化器可以委托同一类,结构或枚举的另一个故障初始化器。类似地,子类可故障初始化器可以委托最多超类可故障初始化器。
在任何一种情况下,如果委托给另一个导致初始化失败的初始化程序,则整个初始化过程将立即失败,并且不会再执行任何初始化代码。
笔记
一个失败的初始化器也可以委派给一个不失败的初始化器。如果您需要将潜在的失败状态添加到现有的初始化过程中,否则不会失败,请使用此方法。
以下示例定义了一个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
实例,则初始化程序将导致初始化失败: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"
覆盖失败的初始化程序
您可以在子类中覆盖超类可失败的初始化程序,就像其他任何初始化程序一样。或者,您可以使用子类不可失败的初始化程序来覆盖超类可失败的初始化程序。这样,即使超类的初始化被允许失败,您也可以定义一个不会失败的子类。
请注意,如果使用不可失败的子类初始化器覆盖了可失败的超类初始化器,则委派给超类初始化器的唯一方法是强制展开可失败的超类初始化器的结果。
笔记
您可以使用不可失败的初始值设定项来覆盖可失败的初始设定项,但反之则不能。
下面的示例定义了一个名为的类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
覆盖其超类的failableinit?(name:)
与nonfailable初始化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!
反之亦然。您也可以委托frominit
到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"