Swift4.2 构造过程

实例的准备过程,这个过程包含了设置实例每个存储属性的初始值并在实例使用之前执行全部所需的其他设置或初始化。


构造过程 就是使用类,结构体或枚举

通过定义构造器来实现构造过程,这更像调用创建某一类型实例的特殊方法。和 Objective-C 的构造器不同,Swift 的构造器不用返回值。它们主要的作用就是确保在第一次使用前某类型的实例都能正确的初始化。

类的实例也可以实现析构器,在该类实例释放前执行自定义的清理工作。关于析构器的更多信息,请参阅 析构过程.

设置存储属性的初始值

类和结构体在其创建实例时 必须 为它们所有的存储属性设置适当的初始值。存储属性不能处于未知状态

你可以在构造器中为存储属性设置初始值,或是作为定义属性时的一部分设置其默认值。详情如下。

注意
当你为存储属性设置默认值时,或是在构造器中设置其初始值,属性值是直接设置的,并不会调用任何属性观察器。

构造器

构造器 在创建某类实例时调用。其最简单的形式用 init 关键字来写,就像一个不带参数的实例方法:

1
2
3
init() {
// 在这执行初始化设置
}

下面的例子定义了一个名为 Fahrenheit 的新结构体,用于保存华氏温度。Fahrenheit 结构体有一个 Double 类型的存储属性 temperature

1
2
3
4
5
6
7
8
9
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// 打印 "The default temperature is 32.0° Fahrenheit"

这个结构体定义了一个没有参数的构造器 init,并将存储属性 temperature 的值初始化为 32.0 ( 水冰点的华氏温度 )。

默认属性值

如上所示,你可以在构造器中设置一个存储属性的初始值。或指定一个 默认属性值 作为声明属性的一部分。指定默认属性值你可以通过在属性定义时为其赋一个初始值。

注意
如果一个属性总是相同的初始值,与其在构造器中设置一个值不如提供一个默认值。其效果是相同的,但是默认值与属性构造器的联系更紧密一些。它使构造器更简短,更清晰,并且可以通过默认值推断属性类型。默认值也使你更易使用默认构造器和构造器继承,本章稍后会详细解释。

你可以通过上述为 temperature 属性提供默认值的简单形式来写结构体 Fahrenheit

1
2
3
struct Fahrenheit {
var temperature = 32.0
}

自定义构造过程

如下所述,你可以通过输入参数和可选类型属性,或在构造过程中给常量属性赋值来自定义构造过程。

构造参数

你可以提供 构造参数 作为构造器定义的一部分,以定义自定义构造过程中值的类型和名字。构造参数有着与函数参数和方法参数相同的功能和语法。

下面的例子定义了一个名为 Celsius 的结构体,用于存储摄氏度温度。结构体Celsius 实现了名为 init(fromFahrenheit:)init(fromKelvin:) 的两个构造器,使用不同单位温度的值初始化了一个结构体实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的结构体,并有名为 redgreenblue 三个常量。这三个属性存储了 0.01.0 之间的值,用于表示颜色中红,绿,蓝的值。

Color 提供了一个带有红,蓝,绿三个 Double 类型参数的构造器。Color 也提供了只带有 white 参数的第二个构造器,用于给三个颜色组件设置相同的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 实例:

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

注意如果不使用外部参数名是无法调用这些构造器的。如果在构造器中定义了外部参数名就必须使用,忽略它将会触发编译错误。

1
2
let veryGreen = Color(0.0, 1.0, 0.0)
// 这会报编译错误 - 必须使用外部参数名

无需外部参数名的构造参数

如果你不想对构造参数使用外部参数名,写一个下划线(_)来代替显式外部参数名以重写其默认行为。

这有一个之前 构造参数 中 Celsius 例子的扩展版本,使用已经是摄氏温度的 Double 值传入额外构造器以创建一个新的 Celsius 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的类,并有一个名为 response 的可选类型其关联值类型为 String 类型的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
// 打印 "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

提出调查问题后,才能知道答案,所以声明 response 属性为 String? 类型,或是『 optional String』。当 SurveyQuestion 的实例被初始化时,它会自动被赋值为 nil,意味着『 还没有字符串 』。

在构造过程期间给常量赋值

构造过程期间你可以在任何时间点给常量属性赋值,只要构造完成时设置了确定值即可。一旦常量属性被赋值,就不能再次修改。

注意
对于类的实例来说,常量属性只能在定义常量属性类的构造器中修改。不能在派生类中修改。

你可以修改上面 SurveyQuestion 的例子,使用常量属性而不是变量属性来表示问题 text ,用于指明一旦创建了 SurveyQuestion 实例,问题就不会再次修改。即使 text 属性现在是一个常量,但你仍然可以在类的构造器中设置其值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
// 打印 "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

默认构造器

Swift 为属性均有默认值和没有构造器的结构体或类提供了一个 默认构造器 。默认构造器创建了一个所有属性都有默认值的新实例。

例子中定义了一个名为 ShoppingListItem 的类,并封装了购物清单中物品的名字,数量和购买状态:

1
2
3
4
5
6
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()

因为 ShoppingListItem 类的所有属性都有默认值,而且它是一个没有父类的基类,所以 ShoppingListItem 自动获得了一个默认构造器以创建一个新的实例,并为其所有属性设置默认值。( name 属性是一个关联值类型为 String 的可选类型,虽然代码中没有写,但它会自动接收默认值 nil。) 上面的例子使用 ShoppingListItem() 这种形式的构造语法调用默认构造器为 ShoppingListItem 类创建了一个新的实例,并将这个新的实例赋值给名为 item 的变量。

结构体类型的成员构造器

如果结构体没有任何自定义构造器,那么结构体类型会自动接收一个 成员构造器。不同于默认构造器,即使结构体的存储属性没有默认值,它也会接收成员构造器。

成员构造器是初始化结构体实例所有成员属性的便捷方法。实例的属性初始值通过参数名称传给成员构造器。

下面的例子定义了一个名为 Size 的结构体并有名为 widthheight 的两个属性。通过为属性分配一个默认值 0.0 从而推断出属性类型是 Double

Size 结构体自动接收了成员构造器 init(width:height:),用于初始化一个新的 Size 实例:

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

值类型的构造器代理

构造器可以调用其他构造器来执行实例的部分构造过程。这个过程称之为 构造器代理 ,以避免多个构造器之间的重复代码。

构造器代理的工作规则和形式规则都不同于值类型或类类型。值类型( 结构体和枚举 )不支持继承,所以其构造代理过程相对简单,因为他们只能代理给自己提供的其他构造器。如 继承 中所述,类是可以继承于其他类的。这意味着类有确保在构造期间将继承来的存储属性合理赋值的额外责任。这些责任稍后会在 类的继承和构造过程 中介绍。

对于值类型,在自定义构造器中使用 self.init 来引用同一类型中的其他构造器。你只能在构造器中调用 self.init

如果你为值类型定义了一个自定义构造器,你将无法再访问该类型的默认构造器( 如果是结构体就是成员构造器 )。这个约束避免了一种缺陷,就是某人使用了某个自动构造器而意外绕开了一个带有额外必要设置且更复杂的构造器。

注意
如果你想让你的自定义类型可以使用默认构造器,成员构造器,自定义构造器来进行初始化,就把自定义构造器写在扩展中,而不是作为值类型原始实现的一部分。更多详情,请参阅 扩展。

下面的例子定义了一个用以表示几何矩形的结构体 Rect 。这个例子需要名为 SizePoint 的两个辅助结构体,它们都为自己所有属性提供了一个默认值 0.0

1
2
3
4
5
6
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}

你可以使用以下三种方式中的任意一种来初始化结构体 Rect— 使用默认值为 0originsize 属性,提供指定的 originsize,或是提供指定的 centersize。在结构体的定义中用三种自定义构造器来表示这三种构造方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}

第一个 Rect 构造器,init(),与结构体没自定义构造器时接收的默认构造器功能相同。用一堆大括号 {} 来表示此构造器为空。调用这个构造器返回一个 Rect 实例,其 originsize 属性使用属性定义时的默认值 Point(x: 0.0, y: 0.0)Size(width: 0.0, height: 0.0) 来初始化:

1
2
let basicRect = Rect()
// basicRect 的 origin 是( 0.0,0.0 ),它的 size 是( 0.0,0.0 )

第二个 Rect 构造器,init(origin:size:),与结构体没自定义构造器时接收的成员构造器功能相同。这个构造器只是简单的把 originsize 参数值赋值给适当的存储属性:

1
2
3
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// originRect 的 origin 是( 2.0,2.0 ),它的 size 是( 5.0,5.0 )

第三个 Rect 构造器,init(center:size:),稍微有些复杂。先基于 centersize 的值计算了合适的原点。然后调用( 或是代理给 )构造器 init(origin:size:) 以在合适的属性中存储新的 originsize

1
2
3
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// // centerRect 的 origin 是( 2.5,2.5 ),它的 size 是( 3.0,3.0 )

构造器 init(center:size:) 可以把 originsize的新值赋给合适的属性。然而构造器 init(center:size:) 利用已提供了相同功能的现有构造器会更方便( 并且意图更清晰 )。

类的继承和构造过程

类的所有存储属性 — 包括任何从父类继承而来的属性 — 必须 在构造过程期间赋值。

Swift 给类类型定义了两种构造器以确保所有存储属性都能接收到初始值。它们分别是指定构造器和便利构造器。

指定构造器和便利构造器

指定构造器 是类的主要构造器。一个指定构造器初始化该类引入的所有属性,并调用合适的父类构造器以继续父类链上的构造过程。

类往往只有很少的指定构造器,通常一个类只有一个指定构造器。指定构造器是构造过程发生的『漏斗(funnel)』点,通过该点将构造过程持续到其父类链。

每个类至少要有一个指定构造器。如同稍后在 自动构造器的继承 中描述的那样,一些情况下,这个条件都是继承父类的一个或多个指定构造器而满足的。

其次是 便利构造器,类的辅助构造器。你可以定义便利构造器来调用同一类中的指定构造器并为指定构造器的一些参数设置默认值。你也可以定义便利构造器为特殊用例类或是输入类型类创建实例。

你应当只在你的类需要时而为其提供便利构造器。相比普通的构造模式,创建便利构造器会节省很多时间并将类的构造过程变得更加清晰。

指定构造器和便利构造器的语法

类的指定构造器和值类型的简单构造器写法相同:

1
2
3
init(parameters) {
statements
}

便利构造器有着相同风格的写法,但是在 init 关键字之前需要放置 convenience 修饰符,并使用空格来分隔:

1
2
3
convenience init(parameters) {
statements
}

类的构造代理

为了简化指定构造器和便利构造器之间的关系。Swift 对构造器之间的代理采用了如下三条规则:

规则 1

  • 指定构造器必须调用其直系父类的指定构造器。

规则 2

  • 便利构造器必须调用 同一 类中的其他构造器。

规则 3

  • 便利构造器最后必须调用指定构造器。

简单的记忆方法:

  • 指定构造器必须 向上 代理。
  • 便利构造器必须 横向 代理。

下图解释了这些规则

父类有一个指定构造器和两个便利构造器。一个便利构造器调用另一个便利构造器,后者又调用指定构造器。这符合上述规则 2 和规则 3。 父类本身并没有父类,所以规则 1 不适用。

该图中的派生类有两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的一个,因为它只能调用同一类中的其他构造器。这符合上述规则 2 和规则 3。两个指定构造器必须调用父类中唯一的指定构造器,所以也符合上述规则 1。

注意
这些规则并不影响每个类 创建 实例。上图中的任何构造器都可以用于为其所属类创建完全初始化的实例。这些规则只会影响类的构造器实现。

下图展示了一个关于四个类之间更复杂的层级结构。它解释了这个层级结构中的指定构造器如何在类的构造过程中饰演『 漏斗(funnel)』点,并简化了构造链中类之间的关系:

两段式构造器过程

Swift 中类的构造过程是两段式处理。第一阶段,为类引入的每个存储属性赋一个初始值。一旦确定了所有存储属性的初始状态,第二阶段开始,在新的实例被认为可以使用前,每个类都有机会进一步定制其存储属性。

两段式构造过程的使用让构造过程更安全,同时对于类层级结构中的每个类仍然给予完全的灵活性。两段式构造过程防止了属性在初始化前访问其值,并防止其他构造器意外给属性赋予不同的值。

注意
Swift 的两段式构造过程类似于 Objective-C 的构造过程。主要区别就是在第一阶段期间,Objective-C 对每个属性赋值为 0 或空(例如 0nil),Swift 的构造过程的流程就更灵活,允许设置自定义初始值,并能应付一些 0nil 不能作为有效默认值的类型。

Swift 的编译器执行了四个有帮助的安全检查以确保两段式构造过程无误完成:

安全检查 1

指定构造器必须确保其类引入的所有属性在向上代理父类构造器之前完成初始化。

如上所述,一个对象的内存只在其所有存储属性初始状态已知时才被认为完全初始化。为了符合此规则,指定构造器必须确保其所属类拥有的属性在向上代理前完成初始化。

安全检查 2

指定构造器必须在继承属性赋值前向上代理父类构造器,否则,便利构造器赋予的新值将被父类构造过程的一部分重写。

安全检查 3

便利构造器必须在 任何 属性(包括同一类中定义的属性)赋值前代理另一个构造器。否则便利构造器赋予的新值将被其所属类的指定构造器重写。

安全检查 4

构造器在第一阶段构造过程完成前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。


类实例在第一阶段完成前并不是完全有效的。一旦第一阶段结束,类实例才是有效的,才能访问属性,调用方法。

以下是基于以上四个安全检查的两段式构造过程的流程:

阶段 1

  • 在类中调用指定或便利构造器。
  • 对一个新实例分配内存,但内存没还没有初始化。
  • 指定构造器确认其所属类的所有存储属性都有值。现在那些存储属性的内存初始化完成。
  • 指定构造器移交给父类构造器以为其存储属性执行相同的任务。
  • 这个过程沿着类的继承链持续向上,直到到达继承链的顶端。
  • 一旦到达链的顶端,并且链中最后的类确保其所有存储属性都有值,则认为实例的内存已经完全初始化,至此阶段 1 完成。

阶段 2

  • 从链顶端往下,链中每个指定构造器都可以选择进一步定制实例,构造器现在可以访问 self 并修改它的属性,调用实例方法,等等。
  • 最终,在链中的任何便利构造器也都可以选择定制实例以及使用 self 。

以下展示了假设的派生类和父类之间的构造过程阶段 1:

在这个例子中,构造过程开始于在派生类上调用一个便利构造器。便利构造器还不能修改任何属性。它横向代理了同一类中的指定构造器。

根据安全检查 1,指定构造器确保派生类的所有属性都有值,然后在它的父类上调用指定构造器以持续链上的构造过程。

父类的指定构造器确保父类所有属性都有值。这里没有父类要初始化,所以所无需进一步代理。

一旦父类的所有属性都有了初始值,就认为其内存完全初始化,此时阶段 1 完成。

以下展示了同一构造过程 阶段 2 :

父类的指定构造器现在有机会进一步定制实例了(尽管不做也可以)。

一旦父类的指定构造器调用结束,派生类的指定构造器就可以执行额外的自定义设置(尽管这也可不做)。

最终,派生类的指定构造器调用结束,最初调用的便利构造器可执行额外的自定义设置。

构造器的继承和重写

与 Objective-C 的派生类不同,Swift 的派生类默认不继承其父类构造器。Swift 这种机制防止了更定制化的派生类继承父类的简单构造器,也防止将简单构造器用于创建不完全初始化或是错误初始化的派生类实例。

注意
在安全和适合的情况下,父类构造器是可以继承的。详情请参阅 自动构造器的继承

如果你想要自定义派生类有一个或多个与其父类相同的构造器,你可以在子类中提供这些自定义构造器的实现。

当你在写一个与父类 指定 构造器相匹配的派生类构造器时,你是在有效的重写指定构造器。因此,你必须在派生类构造器的定义前写上修饰符 override 。就像 默认构造器 中描述的那样,即使你重写的是一个自动提供的默认构造器,也要写上 override

重写属性,方法或是下标,修饰符 override 的存在提示着 Swift 去检查父类是否有匹配的指定构造器用于重写,并验证重写构造器的参数是否已按预期指定。

注意
重写父类指定构造器时总是要写修饰符 override 的,即使你实现的是派生类的便利构造器。

相反的,如果你写一个与父类 便利 构造器相匹配的派生类构造器,根据 类的构造器代理 规则,派生类是不能直接调用父类便利构造器的。因此,你的派生类(严格来说)没有重写父类构造器。所以,在提供与父类便利构造器相匹配的实现时,无需编写 修饰符 override

下面的例子定义了一个名为 Vehicle 的基类。这个基类声明了一个名为 numberOfWheels 的存储属性,其默认值为 Int 类型的 0。名为 description 的计算属性用 numberOfWheels 属性创建一个表示交通工具特性的 String 类型描述。

1
2
3
4
5
6
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}

Vehicle 类为它仅有的存储属性提供了一个默认值,并且没有提供任何自定义构造器。因此,它自动接收了一个默认构造器,就如同 默认构造器 中描述的那样。默认构造器(在可用时)总是类中的指定构造器,并且可以用于创建一个 numberOfWheels0 的新 Vehicle 实例:

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

下面的例子定义了 Vehicle 的派生类 Bicycle

1
2
3
4
5
6
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}

派生类 Bicycle 定义了一个自定义构造器init()。这个指定构造器匹配于 Bicycle 父类的指定构造器,所以这个 Bicycle 版本的构造器使用修饰符 override 标记。

Bicycle 的构造器 init() 首先调用 super.init(),它调用了 Bicycle 父类 Vehicle 的默认构造器。这确保在 Bicycle 有机会修改属性前 Vehicle 完成继承属性 numberOfWheels 的初始化。调用 super.init() 后,将 numberOfWheels 的原始值替换为新值 2

如果你创建一个 Bicycle 的实例,你可以调用它的计算继承属性 description 以查看numberOfWheels 属性是如何更新的:

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

注意
派生类可在构造过程期间可修改变量继承属性,但不能修改常量继承属性。

自动构造器的继承

如上所述,派生类默认不继承其父类构造器。然而,如果满足某些特定条件,父类构造器 是 可以被自动继承的。实际上,这意味着很多常见场景中你不需要重写构造器,并且可以安全的以最小代价继承父类构造器。

假设你为派生类引入的所有属性提供了默认值,请应用以下两条规则以达到自动继承的目的:

规则 1

如果你的派生类没有定义任何指定构造器,它会自动继承其父类的所有指定构造器。

规则 2

如果你的派生类为其父类的 所有 指定构造器都提供了实现 — 无论是按照规则 1 继承而来,或是定义时提供了自定义实现 — 它都会自动继承父类的所有便利构造器。

甚至在派生类进一步添加便利构造器时,这些规则仍然适用。

注意
派生类可以用便利构造器实现父类的指定构造器以作为满足规则 2 的一部分。

实践指定构造器和便利构造器

下例在实践中展示了指定构造器,便利构造器和自动构造器的继承。该例定义了关于 FoodRecipeIngredientShoppingListItem 的层级关系,并演示了它们的构造器是如何相互作用的。

层级结构中的基类名为 Food,它是一个包装食品名称的简单类。Food 引入了一个名为 nameString 类型属性,并且提供了两个构造器以创建 Food 实例:

1
2
3
4
5
6
7
8
9
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}

下图展示了构造链中的 Food 类:

类没有默认成员构造器,所以 Food 类提供了一个带有单个参数 name 的指定构造器。这个构造器可以使用一个指定的 name 属性值创建新的 Food 实例。

1
2
let namedMeat = Food(name: "Bacon")
// namedMeat 的 name 是 "Bacon"

Food 类的构造器 init(name: String) 是一个 指定 构造器,因为它确保了 Food 实例的所有存储属性被完全初始化。Food 类没有父类,所以构造器 init(name: String) 不需要调用 super.init() 完成其构造过程。

Food 类也提供了一个没有参数的 便利 构造器 init()。便利构造器 init() 横向代理 Food 类的指定构造器 init(name: String),并为其构造参数 name 传值 [Unnamed] 以为新食物提供一个默认占位名。

1
2
let mysteryMeat = Food()
// mysteryMeat 的 name 是 "[Unnamed]"

层级结构中的第二个类是 Food 的派生类 RecipeIngredientRecipeIngredient 类模型是食谱中的一种配料。它引入了一个 Int 类型的属性 quantity(以及从父类 Food 继承而来的 name 属性)并定义了两个创建 RecipeIngredient 实例的构造器:

1
2
3
4
5
6
7
8
9
10
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 的值赋给RecipeIngredient 唯一引入的新属性 quantity。然后,构造器向上代理 Food类的构造器 init(name: String)。这个过程符合上述 两段式构造过程 中的安全检查 1。

RecipeIngredient 也定义了一个便利构造器,init(name: String),用于单独使用 name 来创建一个 RecipeIngredient 实例。这个便利构造器假设任何 RecipeIngredient实例的 quantity 为 1,所以不需要显示指定 quantity 即可创建实例。便利构造器的定义可以更快捷,方便的创建 RecipeIngredient 实例,并在创建多个 quantity 为 1 的 RecipeIngredient 实例时避免重复代码。这个便利构造器只是简单的横向代理了类的指定构造器,并为 quantity 传值 1。

RecipeIngredient 类的便利构造器 init(name: String) 使用与 Food 类的 指定 构造器 init(name: String) 相同的参数。因为这个便利构造器就是重写了父类的指定构造器,所以必须使用修饰符 override 标记(详情请见 构造器的继承与重写)。

即使 RecipeIngredient 提供了一个便利构造器 init(name: String),但是由于 RecipeIngredient 为父类所有指定构造器提供了实现。因此,RecipeIngredient 也自动继承了父类的所有便利构造器。

这个例子中,RecipeIngredient 的父类是 Food,它有一个便利构造器 init()。因此这个构造器由 RecipeIngredient 继承。继承版本的 init() 方法与 Food 版本的相同,除了它代理了 RecipeIngredient 版本的 init(name: String),而不是 Food 的。

这三个构造器都可以用于创建新的 RecipeIngredient 实例:

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

层级结构中的第三个也是最后一个类是 RecipeIngredient 的派生类 ShoppingListItem。ShoppingListItem 类模型是购物清单中出现的一种食谱配料。

购物清单中的每项物品开始时都是『未购买状态』。为了表示这一事实,ShoppingListItem引入 Boolean 类型属性 purchased,其默认值是false。ShoppingListItem 也添加了一个计算属性 description,用于为 ShoppingListItem 实例提供文本描述:

1
2
3
4
5
6
7
8
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}

注意
ShoppingListItem 没有定义为 purchased 提供初始值的构造器,因为添加到购物清单中的物品(如同模型中那样)总是未购买的。

因为它为所有属性提供了一个默认值并且没有定义任何构造器,所以ShoppingListItem 自动继承了它父类的 所有 指定构造器和便利构造器。

下图展示了三个类的整体构造链:

三个继承构造器都可以用于创建新的 ShoppingListItem 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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 实例的名字从 "[Unnamed]" 修改为 "Orange juice" 并被标记为已购买。打印数组中每项物品的描述会显示它们的默认状态已按预期设置。

可失败构造器

有时定义类,结构体或是枚举,构造过程可以失败是很有帮助的。无效的构造参数值可能会触发这种失败,或是缺失某种需要的外部资源,又或是未能满足某种条件。

为了应对可能失败的构造过程,你可以为类,结构体,或是枚举定义一个或是多个可失败构造器。编写可失败构造器的语法就是在 init 关键字后面添加问号(init?)。

注意
你不能使用相同的参数类型或参数名定义一个可失败构造器后又定义一个非失败构造器。

可失败构造器会创建一个关联值类型是自身构造类型的 可选 类型。在可失败构造器中编写 return nil 以表示可以在任何情况下触发失败。

注意
严格来说,构造器没有返回值。它们的作用是确保构造过程结束时 self 可以完全并正确的初始化。虽然你写 return nil 是用于触发构造器失败,但表示构造成功是不会使用 return 关键字的。

例如,为数字类型转换实现可失败构造器。使用构造器 init(exactly:) 以确保数字类型之间的转换可以保持精准值。如果类型转换不能保持值不变,则构造失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let wholeNumber: Double = 12345.0
let pi = 3.14159

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

let valueChanged = Int(exactly: pi)
// valueChanged 的类型是 Int?,不是 Int

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

下面的例子定义了结构体 Animal,它有一个 String 类型的常量属性 speciesAnimal 结构体也定义了一个只有一个构造参数 species 的可失败构造器。该构造器检查传递给构造器的 species 参数值是否为空字符串。如果是空字符串,则触发构造失败。否则,将其赋值给 species 属性,构造过程成功:

1
2
3
4
5
6
7
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}

你可以使用可失败构造器尝试初始化一个 Animal 实例,并且检查是否构造成功:

1
2
3
4
5
6
7
let someCreature = Animal(species: "Giraffe")
// someCreature 的类型是 Animal?,不是 Animal

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

如果你给可失败构造器的 species 参数传了一个空字符串,构造器就会触发构造失败:

1
2
3
4
5
6
7
let anonymousCreature = Animal(species: "")
// anonymousCreature 的类型是 Animal?,不是 Animal

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

注意
检查空字符串值(例如 "" 而不是 "Giraffe")和检查值为 nil 的关联值类型是 String 的 可选 类型是完全不同的概念。在上面的例子中,一个 String 类型的空字符串("")是有效的。然而,空字符串作为 animalspecies 属性值明显不合适。因此要限制模型,如果是空字符串可失败构造器应该触发构造失败。

枚举的可失败构造器

你可以使用可失败构造器选择基于一个或多个参数的枚举成员。如果提供的参数不符合任何一个枚举成员,则构造失败。

下例定义了枚举 TemperatureUnit,以及三种可能有的状态
(kelvincelsiusfahrenheit)。可失败构造器用于找到一个与代表温度符号的 Character 类型的值相符合的枚举成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}
}
}

你可以使用可失败构造器在三种可能的状态中选择一个合适的枚举成员,如果参数与三个状态中的任何一个都不匹配就会引起构造失败:

1
2
3
4
5
6
7
8
9
10
11
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// 打印 "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.")
}
// 打印 "This is not a defined temperature unit, so initialization failed."

带有原始值枚举的可失败构造器

带原始值的枚举会自动接收一个可失败构造器 init?(rawValue:),它有一个名为 rawValue 的参数,其类型是相应的原始值类型,如果找到了相匹配的枚举成员,就构造该枚举成员,否则构造失败。

你可以使用 Character 类型的原始值重写上例的 TemperatureUnit 以更好的利用构造器 init?(rawValue:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.")
}
// 打印 "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.")
}
// 打印 "This is not a defined temperature unit, so initialization failed."

构造失败的传递

类、结构体或枚举的可失败构造器可以横向代理同一类型中的其他可失败构造器。类似的,派生类的可失败构造器可以向上代理其父类的可失败构造器。

任意情况下,如果你代理了其他构造器而导致构造失败。整个构造过程立即失败,不再进一步执行构造代码。

注意
一个可失败构造可以代理一个非失败构造器。如果你需要添加一个可能失败的状态到现有构造过程,请使用这个办法,否则将会构造失败。

下例定义了 Product 的派生类 CartItemCartItem 类模型是一个在线购物车中的物品。CartItem 引入了一个常量存储属性 quantity 并确保这个属性值起码为 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 或更大的 quantity。如果 quantity 是无效的,整个构造过程立即失败,构造代码不再执行。同样,Product 检查可失败构造器的 name 值,如果 name 是空字符串构造会立即失败。

如果你使用非空的 name,值为 1 或是更大值的 quantity 创建了一个 CartItem 实例,那么构造成功:

1
2
3
4
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// 打印 "Item: sock, quantity: 2"

如果你尝试使用值为 0quantity 创建一个 CartItem 实例,CartItem 构造器则会引发构造过程失败:

1
2
3
4
5
6
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
print("Unable to initialize zero shirts")
}
// 打印 "Unable to initialize zero shirts"

相似的,如果你尝试使用空 name 值去创建一个 CartItem 实例,其父类 Product 的构造器也会引发构造过程失败:

1
2
3
4
5
6
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product")
}
// 打印 "Unable to initialize one unnamed product"

重写可失败构造器

你可以在派生来中重写父类的可失败构造器,就像其他构造器那样。或是你可以用派生类的 非失败 构造器重写父类的可失败构造器。这允许你定义一个不会构造失败的派生类,即使父类的构造过程允许失败。

注意如果你使用非失败的派生类构造器重写了可失败的父类构造器,向上代理父类构造器的唯一的方法就是强制解包(force-unwrap)可失败父类构造器的结果。

注意
你可以使用非失败构造器重写可失败构造器,但是反过来不行。

下例定义了一个类 Document。这个类模型是一个文档,它可以使用属性值为非空字符串或是 nilname 初始化,但不能是空字符串:

1
2
3
4
5
6
7
8
9
10
class Document {
var name: String?
// 该构造器使用值为 nil 的 name 创建文档
init() {}
// 该构造器使用非空值的 name 创建文档
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

下面的例子定义了 Document 的子类 AutomaticallyNamedDocument,派生类 AutomaticallyNamedDocument 重写了继承自 Document 的两个指定构造器。如果实例没有使用 name 初始化,或是传递给构造器 init(name:) 的参数值是空字符串,重写构造器确保了 AutomaticallyNamedDocument 实例的 name 属性有一个初始值"[Untitled]"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 使用与父类不同的方式处理了空字符串,所以不在需要可失败构造器,因此提供了一个非失败版本的构造器代替继承而来的可失败构造器。

作为实现派生类非失败构造器实现的一部分,你可以在构造器中强制解包以调用父类的可失败构造器。例如,下面的派生类 UntitledDocumentname 总是 "[Untitled]",并且它在构造器期间使用了父类的可失败构造器 init(name:)

1
2
3
4
5
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}

这个例子中,如果调用父类的构造器 init(name:) 时传入空字符串作为 name 的值,强制解包操作将会导致运行时错误。然而,由于它使用的是常量字符串,你可以看见构造器并不会失败,所以这个例子中也不会发生运行时错误。

init! 可失败构造器

通常使用在关键字 init 后面放置问号(init?)的方式来定义一个可失败构造器,用于创建一个适当的可选类型实例。你也可以定义一个可失败构造器,将其用于创建一个适当的隐式解包可选类型的实例。为了定义这个可失败构造器,在关键字 init 后面用叹号来替代问号(init!)。

你可以从 init? 代理到 init! 反之亦然,并且可以使用 init! 重写 init? 反之亦然。你也可以从 init 代理到 init!,不过在 init! 构造失败时会触发断言。

必要构造器

在类构造器的定义前写修饰符 required 以指明该类的每个派生类必须实现此构造器。

1
2
3
4
5
class SomeClass {
required init() {
// 在这实现构造器
}
}

在每个派生类实现必要构造器时也必须在构造器前面写修饰符 required,以指明构造器要求应用于继承链中所有派生类。重写一个必要指定构造器时无需写修饰符 override

1
2
3
4
5
class SomeSubclass: SomeClass {
required init() {
// 在这实现派生类的必要构造器
}
}

注意
如果派生类符合继承构造器的要求,则无需在派生类中为必要构造器提供显式实现。

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

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

这种函数或闭包通常会创建一个与属性类型相同类型的临时值,为满足预期的初始状态而处理其值,然后返回该临时值作为属性的默认值。

以下是如何使用闭包作为属性默认值的大致轮廓:

1
2
3
4
5
6
7
class SomeClass {
let someProperty: SomeType = {
// 在闭包中创建一个带有默认值的 someProperty
// someValue 的类型必须是 SomeType
return someValue
}()
}

注意那个闭包的结束是在大括号后面尾随一对空括号。这告诉 Swift 立即执行闭包。如果你忽略了这对括号,则为试图将闭包本身赋值给属性,而不是闭包的返回值。

注意
如果你使用闭包初始化属性,记住在闭包执行时其他实例还未初始化。这意味着无法在闭包中访问其他属性值,即使那些属性有默认值。你也不能隐式使用 self 属性,或是调用实例方法。

下例定义了一个结构体 Chessboard,其模型是国际象棋的棋盘。国际象棋是 8 x 8 黑白格交替的棋盘。

为了表示这个棋盘,结构体 Chessboard 有一个属性 boardColors,它是一个存储 64Bool 类型值的数组。在数组中,值为 true 代表黑格,值为 false 代表白格。数组中的第一个值表示棋盘左上角的格子,数组中最后一个元素代表棋盘右下角的格子。

使用闭包设置颜色值并初始化数组 boardColors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 中,一旦完成设置就将 temporary 数组作为闭包的返回值返回。返回的数组保存在 boardColors 中并可以使用效用函数 squareIsBlackAt(row:column:) 查询:

1
2
3
4
5
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// 打印 "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// 打印 "false"