swift可选链

分類 编程语言, swift

可选链式调用

可选链式调用是一种可以在当前值可能为 nil 的可选值上请求和调用属性、方法及下标的方法。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil

注意

Swift 的可选链式调用和 Objective-C 中向 nil 发送消息有些相像,但是 Swift 的可选链式调用可以应用于任意类型,并且能检查调用是否成功。

使用可选链式调用代替强制展开

通过在想调用的属性、方法,或下标的可选值后面放一个问号(?),可以定义一个可选链。这一点很像在可选值后面放一个叹号(!)来强制展开它的值。它们的主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制展开将会触发运行时错误。

为了反映可选链式调用可以在空值(nil)上调用的事实,不论这个调用的属性、方法及下标返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来判断你的可选链式调用是否调用成功,如果调用有返回值则说明调用成功,返回 nil 则说明调用失败。

这里需要特别指出,可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是 Int 类型,则会变为 Int? 类型。

下面几段代码将解释可选链式调用和强制展开的不同。

首先定义两个类 PersonResidence

1
2
3
4
5
6
7
class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

Residence 有一个 Int 类型的属性 numberOfRooms,其默认值为 1Person 具有一个可选的 residence 属性,其类型为 Residence?

假如你创建了一个新的 Person 实例,它的 residence 属性由于是可选类型而将被初始化为 nil,在下面的代码中,john 有一个值为 nilresidence 属性:

1
let john = Person()

如果使用叹号(!)强制展开获得这个 johnresidence 属性中的 numberOfRooms 值,会触发运行时错误,因为这时 residence 没有可以展开的值:

1
2
let roomCount = john.residence!.numberOfRooms
// 这会引发运行时错误

john.residence 为非 nil 值的时候,上面的调用会成功,并且把 roomCount 设置为 Int 类型的房间数量。正如上面提到的,当 residencenil 的时候,上面这段代码会触发运行时错误。

可选链式调用提供了另一种访问 numberOfRooms 的方式,使用问号(?)来替代原来的叹号(!):

1
2
3
4
5
6
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印“Unable to retrieve the number of rooms.”

residence 后面添加问号之后,Swift 就会在 residence 不为 nil 的情况下访问 numberOfRooms

因为访问 numberOfRooms 有可能失败,可选链式调用会返回 Int? 类型,或称为“可选的 Int”。如上例所示,当 residencenil 的时候,可选的 Int 将会为 nil,表明无法访问 numberOfRooms。访问成功时,可选的 Int 值会通过可选绑定展开,并赋值给非可选类型的 roomCount 常量。

要注意的是,即使 numberOfRooms 是非可选的 Int 时,这一点也成立。只要使用可选链式调用就意味着 numberOfRooms 会返回一个 Int? 而不是 Int

可以将一个 Residence 的实例赋给 john.residence,这样它就不再是 nil 了:

1
john.residence = Residence()

john.residence 现在包含一个实际的 Residence 实例,而不再是 nil。如果你试图使用先前的可选链式调用访问 numberOfRooms,它现在将返回值为 1Int? 类型的值:

1
2
3
4
5
6
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印“John's residence has 1 room(s).”

为可选链式调用定义模型类

通过使用可选链式调用可以调用多层属性、方法和下标。这样可以在复杂的模型中向下访问各种子属性,并且判断能否访问子属性的属性、方法和下标。

下面这段代码定义了四个模型类,这些例子包括多层可选链式调用。为了方便说明,在 PersonResidence 的基础上增加了 Room 类和 Address 类,以及相关的属性、方法以及下标。

Person 类的定义基本保持不变:

1
2
3
class Person {
var residence: Residence?
}

Residence 类比之前复杂些,增加了一个名为 rooms 的变量属性,该属性被初始化为 [Room] 类型的空数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}

现在 Residence 有了一个存储 Room 实例的数组,numberOfRooms 属性被实现为计算型属性,而不是存储型属性。numberOfRooms 属性简单地返回 rooms 数组的 count 属性的值。

Residence 还提供了访问 rooms 数组的快捷方式,即提供可读写的下标来访问 rooms 数组中指定位置的元素。

此外,Residence 还提供了 printNumberOfRooms 方法,这个方法的作用是打印 numberOfRooms 的值。

最后,Residence 还定义了一个可选属性 address,其类型为 Address?Address 类的定义在下面会说明。

Room 类是一个简单类,其实例被存储在 rooms 数组中。该类只包含一个属性 name,以及一个用于将该属性设置为适当的房间名的初始化函数:

1
2
3
4
class Room {
let name: String
init(name: String) { self.name = name }
}

最后一个类是 Address,这个类有三个 String? 类型的可选属性。buildingName 以及 buildingNumber 属性分别表示大厦的名称和号码,第三个属性 street 表示大厦所在街道的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName != nil {
return buildingName
} else if let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else {
return nil
}
}
}

Address 类提供了 buildingIdentifier() 方法,返回值为 String?。 如果 buildingName 有值则返回 buildingName。或者,如果 buildingNumberstreet 均有值,则返回两者拼接得到的字符串。否则,返回 nil

通过可选链式调用访问属性

正如 使用可选链式调用代替强制展开 中所述,可以通过可选链式调用在一个可选值上访问它的属性,并判断访问是否成功。

使用前面定义过的类,创建一个 Person 实例,然后像之前一样,尝试访问 numberOfRooms 属性:

1
2
3
4
5
6
7
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印“Unable to retrieve the number of rooms.”

因为 john.residencenil,所以这个可选链式调用依旧会像先前一样失败。

还可以通过可选链式调用来设置属性值:

1
2
3
4
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

在这个例子中,通过 john.residence 来设定 address 属性也会失败,因为 john.residence 当前为 nil

上面代码中的赋值过程是可选链式调用的一部分,这意味着可选链式调用失败时,等号右侧的代码不会被执行。对于上面的代码来说,很难验证这一点,因为像这样赋值一个常量没有任何副作用。下面的代码完成了同样的事情,但是它使用一个函数来创建 Address 实例,然后将该实例返回用于赋值。该函数会在返回前打印“Function was called”,这使你能验证等号右侧的代码是否被执行。

1
2
3
4
5
6
7
8
9
10
func createAddress() -> Address {
print("Function was called.")

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"

return someAddress
}
john.residence?.address = createAddress()

没有任何打印消息,可以看出 createAddress() 函数并未被执行。

通过可选链式调用来调用方法

可以通过可选链式调用来调用方法,并判断是否调用成功,即使这个方法没有返回值。

Residence 类中的 printNumberOfRooms() 方法打印当前的 numberOfRooms 值,如下所示:

1
2
3
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}

这个方法没有返回值。然而,没有返回值的方法具有隐式的返回类型 Void,如 无返回值函数 中所述。这意味着没有返回值的方法也会返回 (),或者说空的元组。

如果在可选值上通过可选链式调用来调用这个方法,该方法的返回类型会是 Void?,而不是 Void,因为通过可选链式调用得到的返回值都是可选的。这样我们就可以使用 if 语句来判断能否成功调用 printNumberOfRooms() 方法,即使方法本身没有定义返回值。通过判断返回值是否为 nil 可以判断调用是否成功:

1
2
3
4
5
6
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
// 打印“It was not possible to print the number of rooms.”

同样的,可以据此判断通过可选链式调用为属性赋值是否成功。在上面的 通过可选链式调用访问属性 的例子中,我们尝试给 john.residence 中的 address 属性赋值,即使 residencenil。通过可选链式调用给属性赋值会返回 Void?,通过判断返回值是否为 nil 就可以知道赋值是否成功:

1
2
3
4
5
6
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
// 打印“It was not possible to set the address.”

通过可选链式调用访问下标

通过可选链式调用,我们可以在一个可选值上访问下标,并且判断下标调用是否成功。

注意

通过可选链式调用访问可选值的下标时,应该将问号放在下标方括号的前面而不是后面。可选链式调用的问号一般直接跟在可选表达式的后面。

下面这个例子用下标访问 john.residence 属性存储的 Residence 实例的 rooms 数组中的第一个房间的名称,因为 john.residencenil,所以下标调用失败了:

1
2
3
4
5
6
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// 打印“Unable to retrieve the first room name.”

在这个例子中,问号直接放在 john.residence 的后面,并且在方括号的前面,因为 john.residence 是可选值。

类似的,可以通过下标,用可选链式调用来赋值:

1
john.residence?[0] = Room(name: "Bathroom")

这次赋值同样会失败,因为 residence 目前是 nil

如果你创建一个 Residence 实例,并为其 rooms 数组添加一些 Room 实例,然后将 Residence 实例赋值给 john.residence,那就可以通过可选链和下标来访问数组中的元素:

1
2
3
4
5
6
7
8
9
10
11
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// 打印“The first room name is Living Room.”

访问可选类型的下标

如果下标返回可选类型值,比如 Swift 中 Dictionary 类型的键的下标,可以在下标的结尾括号后面放一个问号来在其可选返回值上进行可选链式调用:

1
2
3
4
5
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// "Dave" 数组现在是 [91, 82, 84],"Bev" 数组现在是 [80, 94, 81]

上面的例子中定义了一个 testScores 数组,包含了两个键值对,分别把 String 类型的键映射到一个 Int 值的数组。这个例子用可选链式调用把 "Dave" 数组中第一个元素设为 91,把 "Bev" 数组的第一个元素 +1,然后尝试把 "Brian" 数组中的第一个元素设为 72。前两个调用成功,因为 testScores 字典中包含 "Dave""Bev" 这两个键。但是 testScores 字典中没有 "Brian" 这个键,所以第三个调用失败。

连接多层可选链式调用

可以通过连接多个可选链式调用在更深的模型层级中访问属性、方法以及下标。然而,多层可选链式调用不会增加返回值的可选层级。

也就是说:

  • 如果你访问的值不是可选的,可选链式调用将会返回可选值。
  • 如果你访问的值就是可选的,可选链式调用不会让可选返回值变得“更可选”。

因此:

  • 通过可选链式调用访问一个 Int 值,将会返回 Int?,无论使用了多少层可选链式调用。
  • 类似的,通过可选链式调用访问 Int? 值,依旧会返回 Int? 值,并不会返回 Int??

下面的例子尝试访问 john 中的 residence 属性中的 address 属性中的 street 属性。这里使用了两层可选链式调用,residence 以及 address 都是可选值:

1
2
3
4
5
6
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// 打印“Unable to retrieve the address.”

john.residence 现在包含一个有效的 Residence 实例。然而,john.residence.address 的值当前为 nil。因此,调用 john.residence?.address?.street 会失败。

需要注意的是,上面的例子中,street 的属性为 String?john.residence?.address?.street 的返回值也依然是 String?,即使已经使用了两层可选链式调用。

如果为 john.residence.address 赋值一个 Address 实例,并且为 address 中的 street 属性设置一个有效值,我们就能过通过可选链式调用来访问 street 属性:

1
2
3
4
5
6
7
8
9
10
11
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// 打印“John's street name is Laurel Street.”

在上面的例子中,因为 john.residence 包含一个有效的 Address 实例,所以对 john.residenceaddress 属性赋值将会成功。

在方法的可选返回值上进行可选链式调用

上面的例子展示了如何在一个可选值上通过可选链式调用来获取它的属性值。我们还可以在一个可选值上通过可选链式调用来调用方法,并且可以根据需要继续在方法的可选返回值上进行可选链式调用。

在下面的例子中,通过可选链式调用来调用 AddressbuildingIdentifier() 方法。这个方法返回 String? 类型的值。如上所述,通过可选链式调用来调用该方法,最终的返回值依旧会是 String? 类型:

1
2
3
4
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// 打印“John's building identifier is The Larches.”

如果要在该方法的返回值上进行可选链式调用,在方法的圆括号后面加上问号即可:

1
2
3
4
5
6
7
8
9
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// 打印“John's building identifier begins with "The".”

注意

在上面的例子中,在方法的圆括号后面加上问号是因为你要在 buildingIdentifier() 方法的可选返回值上进行可选链式调用,而不是 buildingIdentifier() 方法本身。

留言與分享

swift析构过程

分類 编程语言, swift

析构过程

析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit 来标示,类似于构造器要用 init 来标示。

析构过程原理

Swift 会自动释放不再需要的实例以释放资源。如 自动引用计数 章节中所讲述,Swift 通过自动引用计数(ARC) 处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前手动去关闭该文件。

在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数和圆括号,如下所示:

1
2
3
deinit {
// 执行析构过程
}

析构器是在实例释放发生前被自动调用的。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。

因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为(比如查找一个需要被关闭的文件)。

析构器实践

这是一个析构器实践的例子。这个例子描述了一个简单的游戏,这里定义了两种新类型,分别是 BankPlayerBank 类管理一种虚拟硬币,确保流通的硬币数量永远不可能超过 10,000。在游戏中有且只能有一个 Bank 存在,因此 Bank 用类来实现,并使用类型属性和类型方法来存储和管理其当前状态。

1
2
3
4
5
6
7
8
9
10
11
class Bank {
static var coinsInBank = 10_000
static func distribute(coins numberOfCoinsRequested: Int) -> Int {
let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receive(coins: Int) {
coinsInBank += coins
}
}

Bank 使用 coinsInBank 属性来跟踪它当前拥有的硬币数量。Bank 还提供了两个方法,distribute(coins:)receive(coins:),分别用来处理硬币的分发和收集。

distribute(coins:) 方法在 Bank 对象分发硬币之前检查是否有足够的硬币。如果硬币不足,Bank 对象会返回一个比请求时小的数字(如果 Bank 对象中没有硬币了就返回 0)。此方法返回一个整型值,表示提供的硬币的实际数量。

receive(coins:) 方法只是将 Bank 实例接收到的硬币数目加回硬币存储中。

Player 类描述了游戏中的一个玩家。每一个玩家在任意时间都有一定数量的硬币存储在他们的钱包中。这通过玩家的 coinsInPurse 属性来表示:

1
2
3
4
5
6
7
8
9
10
11
12
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.distribute(coins: coins)
}
func win(coins: Int) {
coinsInPurse += Bank.distribute(coins: coins)
}
deinit {
Bank.receive(coins: coinsInPurse)
}
}

每个 Player 实例在初始化的过程中,都从 Bank 对象获取指定数量的硬币。如果没有足够的硬币可用,Player 实例可能会收到比指定数量少的硬币。

Player 类定义了一个 win(coins:) 方法,该方法从 Bank 对象获取一定数量的硬币,并把它们添加到玩家的钱包。Player 类还实现了一个析构器,这个析构器在 Player 实例释放前被调用。在这里,析构器的作用只是将玩家的所有硬币都返还给 Bank 对象:

1
2
3
4
5
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// 打印“A new player has joined the game with 100 coins”
print("There are now \(Bank.coinsInBank) coins left in the bank")
// 打印“There are now 9900 coins left in the bank”

创建一个 Player 实例的时候,会向 Bank 对象申请得到 100 个硬币,前提是有足够的硬币可用。这个 Player 实例存储在一个名为 playerOne 的可选类型的变量中。这里使用了一个可选类型的变量,是因为玩家可以随时离开游戏,设置为可选使你可以追踪玩家当前是否在游戏中。

因为 playerOne 是可选的,所以在访问其 coinsInPurse 属性来打印钱包中的硬币数量和调用 win(coins:) 方法时,使用感叹号(!)强制解包:

1
2
3
4
5
playerOne!.win(coins: 2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// 打印“PlayerOne won 2000 coins & now has 2100 coins”
print("The bank now only has \(Bank.coinsInBank) coins left")
// 打印“The bank now only has 7900 coins left”

在这里,玩家已经赢得了 2,000 枚硬币,所以玩家的钱包中现在有 2,100 枚硬币,而 Bank 对象只剩余 7,900 枚硬币。

1
2
3
4
5
playerOne = nil
print("PlayerOne has left the game")
// 打印“PlayerOne has left the game”
print("The bank now has \(Bank.coinsInBank) coins")
// 打印“The bank now has 10000 coins”

玩家现在已经离开了游戏。这通过将可选类型的 playerOne 变量设置为 nil 来表示,意味着“没有 Player 实例”。当这一切发生时,playerOne 变量对 Player 实例的引用被破坏了。没有其它属性或者变量引用 Player 实例,因此该实例会被释放,以便回收内存。在这之前,该实例的析构器被自动调用,玩家的硬币被返还给银行。

留言與分享

swift构造过程

分類 编程语言, swift

构造过程

构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例使用前有个过程是必须的,它包括设置实例中每个存储属性的初始值和执行其他必须的设置或构造过程。

你要通过定义构造器来实现构造过程,它就像用来创建特定类型新实例的特殊方法。与 Objective-C 中的构造器不同,Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化。

类的实例也可以通过实现析构器来执行它释放之前自定义的清理工作。想了解更多关于析构器的内容,请参 考 析构过程

存储属性的初始赋值

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

你可以在构造器中为存储型属性设置初始值,也可以在定义属性时分配默认值。以下小节将详细介绍这两种方法。

注意

当你为存储型属性分配默认值或者在构造器中为设置初始值时,它们的值是被直接设置的,不会触发任何属性观察者。

构造器

构造器在创建某个特定类型的新实例时被调用。它的最简形式类似于一个不带任何形参的实例方法,以关键字 init 命名:

1
2
3
init() {
// 在此处执行构造过程
}

下面例子中定义了一个用来保存华氏温度的结构体 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。它定义了两个不同的构造器:init(fromFahrenheit:)init(fromKelvin:),二者分别通过接受不同温标下的温度值来创建新的实例:

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
}
}

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius 是 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius 是 0.0

第一个构造器拥有一个构造形参,其实参标签为 fromFahrenheit,形参命名为 fahrenheit;第二个构造器也拥有一个构造形参,其实参标签为 fromKelvin,形参命名为 kelvin。这两个构造器都将单一的实参转换成摄氏温度值,并保存在属性 temperatureInCelsius 中。

形参命名和实参标签

跟函数和方法形参相同,构造形参可以同时使用在构造器里使用的形参命名和一个外部调用构造器时使用的实参标签。

然而,构造器并不像函数和方法那样在括号前有一个可辨别的方法名。因此在调用构造器时,主要通过构造器中形参命名和类型来确定应该被调用的构造器。正因如此,如果你在定义构造器时没有提供实参标签,Swift 会为构造器的每个形参自动生成一个实参标签。

以下例子中定义了一个结构体 Color,它包含了三个常量:redgreenblue。这些属性可以存储 0.01.0 之间的值,用来表明颜色中红、绿、蓝成分的含量。

Color 提供了一个构造器,为红蓝绿提供三个合适 Double 类型的形参命名。Color 也提供了第二个构造器,它只包含名为 whiteDouble 类型的形参,它为三个颜色的属性提供相同的值。

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
15
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 为 37.0

构造器调用 Celsius(37.0) 意图明确,不需要实参标签。因此适合使用 init(_ celsius: Double) 这样的构造器,从而可以通过提供未命名的 Double 值来调用构造器。

可选属性类型

如果你自定义的类型有一个逻辑上允许值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时机可以赋值为空——都需要将它声明为 可选类型。可选类型的属性将自动初始化为 nil,表示这个属性是特意在构造过程设置为空。

下面例子中定义了类 SurveyQuestion,它包含一个可选 String 属性 response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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? 类型,或者说是 “可选类型 String“。当 SurveyQuestion 的实例初始化时,它将自动赋值为 nil,表明“暂时还没有字符“。

构造过程中常量属性的赋值

你可以在构造过程中的任意时间点给常量属性赋值,只要在构造过程结束时它设置成确定的值。一旦常量属性被赋值,它将永远不可更改。

注意

对于类的实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

你可以修改上面的 SurveyQuestion 示例,用常量属性替代变量属性 text,表示问题内容 textSurveyQuestion 的实例被创建之后不会再被修改。尽管 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,它封装了购物清单中的某一物品的名字(name)、数量(quantity)和购买状态 purchase state

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

由于 ShoppingListItem 类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个将为所有属性设置默认值的并创建实例的默认构造器(由于 name 属性是可选 String 类型,它将接收一个默认 nil 的默认值,尽管代码中没有写出这个值)。上面例子中使用默认构造器创造了一个 ShoppingListItem 类的实例(使用 ShoppingListItem() 形式的构造器语法),并将其赋值给变量 item

结构体的逐一成员构造器

结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。

逐一成员构造器是用来初始化结构体新实例里成员属性的快捷方法。新实例的属性初始值可以通过名字传入逐一成员构造器中。

下面例子中定义了一个结构体 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)

当你调用一个逐一成员构造器(memberwise initializer)时,可以省略任何一个有默认值的属性。在上面这个例子中,Size 结构体的 heightwidth 属性各有一个默认值。你可以省略两者或两者之一,对于被省略的属性,构造器会使用默认值。举个例子:

1
2
3
4
5
6
7
let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// 打印 "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// 打印 "0.0 0.0"

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能避免多个构造器间的代码重复。

构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类(请参考 继承)。这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。这些责任将在后续章节 类的继承和构造过程 中介绍。

对于值类型,你可以使用 self.init 在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用 self.init

请注意,如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这种限制避免了在一个更复杂的构造器中做了额外的重要设置,但有人不小心使用自动生成的构造器而导致错误的情况。

注意

假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。想查看更多内容,请查看 扩展 章节。

下面例子定义一个自定义结构体 Rect,用来代表几何矩形。这个例子需要两个辅助的结构体 SizePoint,它们各自为其所有的属性提供了默认初始值 0.0

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

struct Point {
var x = 0.0, y = 0.0
}

你可以通过以下三种方式为 Rect 创建实例——使用含有默认值的 originsize 属性来初始化;提供指定的 originsize 实例来初始化;提供指定的 centersize 来初始化。在下面 Rect 结构体定义中,我们为这三种方式提供了三个自定义的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的值计算出 origin 的坐标,然后再调用(或者说代理给)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:) 通过使用提供了相关功能的现有构造器将会更加便捷(而且意图更清晰)。

注意

如果你想用另外一种不需要自己定义 init()init(origin:size:) 的方式来实现这个例子,请参考 扩展

类的继承和构造过程

类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。

Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们被称为指定构造器和便利构造器。

指定构造器和便利构造器

指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。

类倾向于拥有极少的指定构造器,普遍的是一个类只拥有一个指定构造器。指定构造器像一个个“漏斗”放在构造过程发生的地方,让构造过程沿着父类链继续往上进行。

每一个类都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。具体内容请参考后续章节 构造器的自动继承

便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

你应当只在必要的时候为类提供便利构造器,比方说某种情况下通过使用便利构造器来快捷调用某个指定构造器,能够节省更多开发时间并让类的构造过程更清晰明了。

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

类的指定构造器的写法跟值类型简单构造器一样:

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。

注意

这些规则不会影响类的实例如何创建。任何上图中展示的构造器都可以用来创建完全初始化的实例。这些规则只影响类的构造器如何实现。

下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“漏斗”的作用,在类的构造器链上简化了类之间的相互关系。

复杂构造器代理图

两段式构造过程

Swift 中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。

两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

注意

Swift 的两段式构造过程跟 Objective-C 中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值 0 或空值(比如说 0nil)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以 0nil 作为合法默认值的情况。

Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成:

安全检查 1

指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。

如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。

安全检查 2

指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。

安全检查 3

便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做,便利构造器赋予的新值将被该类的指定构造器所覆盖。

安全检查 4

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

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

以下是基于上述安全检查的两段式构造过程展示:

阶段 1
  • 类的某个指定构造器或便利构造器被调用。
  • 完成类的新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
  • 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
  • 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
  • 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2
  • 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  • 最终,继承链中任意的便利构造器有机会自定义实例和使用 self

下图展示了在假定的子类和父类之间的构造阶段 1:

构建过程阶段1

在这个例子中,构造过程从对子类中一个便利构造器的调用开始。这个便利构造器此时还不能修改任何属性,它会代理到该类中的指定构造器。

如安全检查 1 所示,指定构造器将确保所有子类的属性都有值。然后它将调用父类的指定构造器,并沿着继承链一直往上完成父类的构造过程。

父类中的指定构造器确保所有父类的属性都有值。由于没有更多的父类需要初始化,也就无需继续向上代理。

一旦父类中所有属性都有了初始值,实例的内存被认为是完全初始化,阶段 1 完成。

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

构建过程阶段2

父类中的指定构造器现在有机会进一步自定义实例(尽管这不是必须的)。

一旦父类中的指定构造器完成调用,子类中的指定构造器可以执行更多的自定义操作(这也不是必须的)。

最终,一旦子类的指定构造器完成调用,最开始被调用的便利构造器可以执行更多的自定义操作。

构造器的继承和重写

跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。

注意

父类的构造器仅会在安全和适当的某些情况下被继承。具体内容请参考后续章节 构造器的自动继承

假如你希望自定义的子类中能提供一个或多个跟父类相同的构造器,你可以在子类中提供这些构造器的自定义实现。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上 override 修饰符,具体内容请参考 默认构造器

正如重写属性,方法或者是下标,override 修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否被按预想中被指定。

注意

当你重写一个父类的指定构造器时,你总是需要写 override 修饰符,即使是为了实现子类的便利构造器。

相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文 类的构造器代理规则 有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加 override 修饰符。

在下面的例子中定义了一个叫 Vehicle 的基类。基类中声明了一个存储型属性 numberOfWheels,它是默认值为 Int 类型的 0numberOfWheels 属性用在一个描述车辆特征 String 类型为 descrpiption 的计算型属性中:

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

Vehicle 类只为存储型属性提供默认值,也没有提供自定义构造器。因此,它会自动获得一个默认构造器,具体内容请参考 默认构造器。默认构造器(如果有的话)总是类中的指定构造器,可以用于创建 numberOfWheels0Vehicle 实例:

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 中这个版本的构造器需要带上 override 修饰符。

Bicycle 的构造器 init() 以调用 super.init() 方法开始,这个方法的作用是调用 Bicycle 的父类 Vehicle 的默认构造器。这样可以确保 Bicycle 在修改属性之前,它所继承的属性 numberOfWheels 能被 Vehicle 类初始化。在调用 super.init() 之后,属性 numberOfWheels 的原值被新值 2 替换。

如果你创建一个 Bicycle 实例,你可以调用继承的 description 计算型属性去查看属性 numberOfWheels 是否有改变:

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

如果子类的构造器没有在阶段 2 过程中做自定义操作,并且父类有一个无参数的自定义构造器。你可以在所有父类的存储属性赋值之后省略 super.init() 的调用。

这个例子定义了另一个 Vehicle 的子类 Hoverboard ,只设置它的 color 属性。这个构造器依赖隐式调用父类的构造器来完成,而不是显示调用 super.init()

1
2
3
4
5
6
7
8
9
10
class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
// super.init() 在这里被隐式调用
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}

Hoverboard 的实例用 Vehicle 构造器里默认的轮子数量。

1
2
3
let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

注意

子类可以在构造过程修改继承来的变量属性,但是不能修改继承来的常量属性。

构造器的自动继承

如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:

规则 1

如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。

规则 2

如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。

即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。

注意

子类可以将父类的指定构造器实现为便利构造器来满足规则 2。

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

接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类 FoodRecipeIngredient 以及 ShoppingListItem 的层级结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是 Food,它是一个简单的用来封装食物名字的类。Food 类引入了一个叫做 nameString 类型的属性,并且提供了两个构造器来创建 Food 实例:

1
2
3
4
5
6
7
8
9
10
class Food {
var name: String
init(name: String) {
self.name = name
}

convenience init() {
self.init(name: "[Unnamed]")
}
}

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

Food 构造器链

类类型没有默认的逐一成员构造器,所以 Food 类提供了一个接受单一参数 name 的指定构造器。这个构造器可以使用一个特定的名字来创建新的 Food 实例:

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

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

Food 类同样提供了一个没有参数的便利构造器 init()。这个 init() 构造器为新食物提供了一个默认的占位名字,通过横向代理到指定构造器 init(name: String) 并给参数 name 赋值为 [Unnamed] 来实现:

1
2
let mysteryMeat = Food()
// mysteryMeat 的名字是 [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 构造器

RecipeIngredient 类拥有一个指定构造器 init(name: String, quantity: Int),它可以用来填充 RecipeIngredient 实例的所有属性值。这个构造器一开始先将传入的 quantity 实参赋值给 quantity 属性,这个属性也是唯一在 RecipeIngredient 中新引入的属性。随后,构造器向上代理到父类 Foodinit(name: String)。这个过程满足 两段式构造过程 中的安全检查 1。

RecipeIngredient 也定义了一个便利构造器 init(name: String),它只通过 name 来创建 RecipeIngredient 的实例。这个便利构造器假设任意 RecipeIngredient 实例的 quantity1,所以不需要显式的质量即可创建出实例。这个便利构造器的定义可以更加方便和快捷地创建实例,并且避免了创建多个 quantity1RecipeIngredient 实例时的代码重复。这个便利构造器只是简单地横向代理到类中的指定构造器,并为 quantity 参数传递 1

RecipeIngredient 的便利构造器 init(name: String) 使用了跟 Food 中指定构造器 init(name: String) 相同的形参。由于这个便利构造器重写了父类的指定构造器 init(name: String),因此必须在前面使用 override 修饰符(参见 构造器的继承和重写)。

尽管 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 引入了一个 Boolean(布尔类型) 的属性 purchased,它的默认值是 falseShoppingListItem 还添加了一个计算型属性 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
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 语句来表明可失败构造器在何种情况下应该 “失败”。

注意

严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 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 的结构体,其中有一个名为 speciesString 类型的常量属性。同时该结构体还定义了一个接受一个名为 speciesString 类型形参的可失败构造器。这个可失败构造器检查传入的species 值是否为一个空字符串。如果为空字符串,则构造失败。否则,species 属性被赋值,构造成功。

1
2
3
4
5
6
7
8
9
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 的可选类型的字符串是两个完全不同的概念。上例中的空字符串("")其实是一个有效的,非可选类型的字符串。这里我们之所以让 Animal 的可失败构造器构造失败,只是因为对于 Animal 这个类的 species 属性来说,它更适合有一个具体的值,而不是空字符串。

枚举类型的可失败构造器

你可以通过一个带一个或多个形参的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的形参无法匹配任何枚举成员,则构造失败。

下例中,定义了一个名为 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 形参,选择找到的相匹配的枚举成员,找不到则构造失败。

因此上面的 TemperatureUnit 的例子可以用原始值类型的 Character 和进阶的 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.”

构造失败的传递

类、结构体、枚举的可失败构造器可以横向代理到它们自己其他的可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

注意

可失败构造器也可以代理到其它的不可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。

下面这个例子,定义了一个名为 CartItemProduct 类的子类。这个类建立了一个在线购物车中的物品的模型,它有一个名为 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 可失败构造器首先验证接收的 quantity 值是否大于等于 1 。倘若 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”

倘若你以一个值为 0 的 quantity 来创建一个 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”

重写一个可失败构造器

如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

注意

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

下例定义了一个名为 Document 的类。这个类模拟一个文档并可以用 name 属性来构造,属性的值必须为一个非空字符串或 nil,但不能是一个空字符串:

1
2
3
4
5
6
7
8
9
10
class Document {
var name: String?
// 该构造器创建了一个 name 属性的值为 nil 的 document 实例
init() {}
// 该构造器创建了一个 name 属性的值为非空字符串的 document 实例
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

下面这个例子,定义了一个 Document 类的子类 AutomaticallyNamedDocument。这个子类重写了所有父类引入的指定构造器。这些重写确保了无论是使用 init() 构造器,还是使用 init(name:) 构造器,在没有名字或者形参传入空字符串时,生成的实例中的 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:)。因为子类用另一种方式处理了空字符串的情况,所以不再需要一个可失败构造器,因此子类用一个不可失败构造器代替了父类的可失败构造器。

你可以在子类的不可失败构造器中使用强制解包来调用父类的可失败构造器。比如,下面的 UntitledDocument 子类的 name 属性的值总是 "[Untitled]",它在构造过程中使用了父类的可失败构造器 init?(name:)

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

在这个例子中,如果在调用父类的可失败构造器 init?(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 中,最后在构建完成时将此数组作为闭包返回值返回。这个返回的数组会保存到 boardColors 中,并可以通过工具函数 squareIsBlackAtRow 来查询:

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

留言與分享

swift继承

分類 编程语言, swift

继承

一个类可以继承另一个类的方法,属性和其它特性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)。在 Swift 中,继承是区分「类」与其它类型的一个基本特征。

在 Swift 中,类可以调用和访问超类的方法、属性和下标,并且可以重写这些方法,属性和下标来优化或修改它们的行为。Swift 会检查你的重写定义在超类中是否有匹配的定义,以此确保你的重写行为是正确的。

可以为类中继承来的属性添加属性观察器,这样一来,当属性值改变时,类就会被通知到。可以为任何属性添加属性观察器,无论它原本被定义为存储型属性还是计算型属性。

定义一个基类

不继承于其它类的类,称之为基类

注意

Swift 中的类并不是从一个通用的基类继承而来的。如果你不为自己定义的类指定一个超类的话,这个类就会自动成为基类。

下面的例子定义了一个叫 Vehicle 的基类。这个基类声明了一个名为 currentSpeed,默认值是 0.0 的存储型属性(属性类型推断为 Double)。currentSpeed 属性的值被一个 String 类型的只读计算型属性 description 使用,用来创建对于车辆的描述。

Vehicle 基类还定义了一个名为 makeNoise 的方法。这个方法实际上不为 Vehicle 实例做任何事,但之后将会被 Vehicle 的子类定制:

1
2
3
4
5
6
7
8
9
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
// 什么也不做——因为车辆不一定会有噪音
}
}

可以用初始化语法创建一个 Vehicle 的新实例,即类名后面跟一个空括号:

1
let someVehicle = Vehicle()

现在已经创建了一个 Vehicle 的新实例,你可以访问它的 description 属性来打印车辆的当前速度:

1
2
print("Vehicle: \(someVehicle.description)")
// 打印“Vehicle: traveling at 0.0 miles per hour”

Vehicle 类定义了一个具有通用特性的车辆类,但实际上对于它本身来说没什么用处。为了让它变得更加有用,还需要进一步完善它,从而能够描述一个具体类型的车辆。

子类生成

子类生成指的是在一个已有类的基础上创建一个新的类。子类继承超类的特性,并且可以进一步完善。你还可以为子类添加新的特性。

为了指明某个类的超类,将超类名写在子类名的后面,用冒号分隔:

1
2
3
class SomeClass: SomeSuperclass {
// 这里是子类的定义
}

下一个例子,定义了一个叫 Bicycle 的子类,继承自父类 Vehicle

1
2
3
class Bicycle: Vehicle {
var hasBasket = false
}

新的 Bicycle 类自动继承 Vehicle 类的所有特性,比如 currentSpeeddescription 属性,还有 makeNoise() 方法。

除了所继承的特性,Bicycle 类还定义了一个默认值为 false 的存储型属性 hasBasket(属性推断为 Bool)。

默认情况下,你创建的所有新的 Bicycle 实例不会有一个篮子(即 hasBasket 属性默认为 false)。创建该实例之后,你可以为 Bicycle 实例设置 hasBasket 属性为 ture

1
2
let bicycle = Bicycle()
bicycle.hasBasket = true

你还可以修改 Bicycle 实例所继承的 currentSpeed 属性,和查询实例所继承的 description 属性:

1
2
3
bicycle.currentSpeed = 15.0
print("Bicycle: \(bicycle.description)")
// 打印“Bicycle: traveling at 15.0 miles per hour”

子类还可以继续被其它类继承,下面的示例为 Bicycle 创建了一个名为 Tandem(双人自行车)的子类:

1
2
3
class Tandem: Bicycle {
var currentNumberOfPassengers = 0
}

TandemBicycle 继承了所有的属性与方法,这又使它同时继承了 Vehicle 的所有属性与方法。Tandem 也增加了一个新的叫做 currentNumberOfPassengers 的存储型属性,默认值为 0

如果你创建了一个 Tandem 的实例,你可以使用它所有的新属性和继承的属性,还能查询从 Vehicle 继承来的只读属性 description

1
2
3
4
5
6
let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Tandem: \(tandem.description)")
// 打印:“Tandem: traveling at 22.0 miles per hour”

重写

子类可以为继承来的实例方法,类方法,实例属性,类属性,或下标提供自己定制的实现。我们把这种行为叫重写

如果要重写某个特性,你需要在重写定义的前面加上 override 关键字。这么做,就表明了你是想提供一个重写版本,而非错误地提供了一个相同的定义。意外的重写行为可能会导致不可预知的错误,任何缺少 override 关键字的重写都会在编译时被认定为错误。

override 关键字会提醒 Swift 编译器去检查该类的超类(或其中一个父类)是否有匹配重写版本的声明。这个检查可以确保你的重写定义是正确的。

访问超类的方法,属性及下标

当你在子类中重写超类的方法,属性或下标时,有时在你的重写版本中使用已经存在的超类实现会大有裨益。比如,你可以完善已有实现的行为,或在一个继承来的变量中存储一个修改过的值。

在合适的地方,你可以通过使用 super 前缀来访问超类版本的方法,属性或下标:

  • 在方法 someMethod() 的重写实现中,可以通过 super.someMethod() 来调用超类版本的 someMethod() 方法。
  • 在属性 someProperty 的 getter 或 setter 的重写实现中,可以通过 super.someProperty 来访问超类版本的 someProperty 属性。
  • 在下标的重写实现中,可以通过 super[someIndex] 来访问超类版本中的相同下标。

重写方法

在子类中,你可以重写继承来的实例方法或类方法,提供一个定制或替代的方法实现。

下面的例子定义了 Vehicle 的一个新的子类,叫 Train,它重写了从 Vehicle 类继承来的 makeNoise() 方法:

1
2
3
4
5
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}

如果你创建一个 Train 的新实例,并调用了它的 makeNoise() 方法,你就会发现 Train 版本的方法被调用:

1
2
3
let train = Train()
train.makeNoise()
// 打印“Choo Choo”

重写属性

你可以重写继承来的实例属性或类型属性,提供自己定制的 getter 和 setter,或添加属性观察器,使重写的属性可以观察到底层的属性值什么时候发生改变。

重写属性的 Getters 和 Setters

你可以提供定制的 getter(或 setter)来重写任何一个继承来的属性,无论这个属性是存储型还是计算型属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。你在重写一个属性时,必须将它的名字和类型都写出来。这样才能使编译器去检查你重写的属性是与超类中同名同类型的属性相匹配的。

你可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter 和 setter 即可。但是,你不可以将一个继承来的读写属性重写为一个只读属性。

注意

如果你在重写属性中提供了 setter,那么你也一定要提供 getter。如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过 super.someProperty 来返回继承来的值,其中 someProperty 是你要重写的属性的名字。

以下的例子定义了一个新类,叫 Car,它是 Vehicle 的子类。这个类引入了一个新的存储型属性叫做 gear,默认值为整数 1Car 类重写了继承自 Vehicledescription 属性,提供包含当前档位的自定义描述:

1
2
3
4
5
6
class Car: Vehicle {
var gear = 1
override var description: String {
return super.description + " in gear \(gear)"
}
}

重写的 description 属性首先要调用 super.description 返回 Vehicle 类的 description 属性。之后,Car 类版本的 description 在末尾增加了一些额外的文本来提供关于当前档位的信息。

如果你创建了 Car 的实例并且设置了它的 gearcurrentSpeed 属性,你可以看到它的 description 返回了 Car 中的自定义描述:

1
2
3
4
5
let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Car: \(car.description)")
// 打印“Car: traveling at 25.0 miles per hour in gear 3”

重写属性观察器

你可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,无论被继承属性原本是如何实现的,当其属性值发生改变时,你就会被通知到。关于属性观察器的更多内容,请看 属性观察器

注意

你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供 willSetdidSet 实现也是不恰当。 此外还要注意,你不可以同时提供重写的 setter 和重写的属性观察器。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的 setter,那么你在 setter 中就可以观察到任何值变化了。

下面的例子定义了一个新类叫 AutomaticCar,它是 Car 的子类。AutomaticCar 表示自动档汽车,它可以根据当前的速度自动选择合适的档位:

1
2
3
4
5
6
7
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}

当你设置 AutomaticCarcurrentSpeed 属性,属性的 didSet 观察器就会自动地设置 gear 属性,为新的速度选择一个合适的档位。具体来说就是,属性观察器将新的速度值除以 10,然后向下取得最接近的整数值,最后加 1 来得到档位 gear 的值。例如,速度为 35.0 时,档位为 4

1
2
3
4
let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("AutomaticCar: \(automatic.description)")
// 打印“AutomaticCar: traveling at 35.0 miles per hour in gear 4”

防止重写

你可以通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可(例如:final varfinal funcfinal class func 以及 final subscript)。

任何试图对带有 final 标记的方法、属性或下标进行重写的代码,都会在编译时会报错。在类扩展中的方法,属性或下标也可以在扩展的定义里标记为 final

可以通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。这样的类是不可被继承的,试图继承这样的类会导致编译报错。

留言與分享

swift下标

分類 编程语言, swift

下标

下标可以定义在类、结构体和枚举中,是访问集合、列表或序列中元素的快捷方式。可以使用下标的索引,设置和获取值,而不需要再调用对应的存取方法。举例来说,用下标访问一个 Array 实例中的元素可以写作 someArray[index],访问 Dictionary 实例中的元素可以写作 someDictionary[key]

一个类型可以定义多个下标,通过不同索引类型进行重载。下标不限于一维,你可以定义具有多个入参的下标满足自定义类型的需求。

下标语法

下标允许你通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行存取。语法类似于实例方法语法和计算型属性语法的混合。与定义实例方法类似,定义下标使用 subscript 关键字,指定一个或多个输入参数和返回类型;与实例方法不同的是,下标可以设定为读写或只读。这种行为由 getter 和 setter 实现,有点类似计算型属性:

1
2
3
4
5
6
7
8
subscript(index: Int) -> Int {
get {
// 返回一个适当的 Int 类型的值
}
set(newValue) {
// 执行适当的赋值操作
}
}

newValue 的类型和下标的返回类型相同。如同计算型属性,可以不指定 setter 的参数(newValue)。如果不指定参数,setter 会提供一个名为 newValue 的默认参数。

如同只读计算型属性,可以省略只读下标的 get 关键字:

1
2
3
subscript(index: Int) -> Int {
// 返回一个适当的 Int 类型的值
}

下面代码演示了只读下标的实现,这里定义了一个 TimesTable 结构体,用来表示传入整数的乘法表:

1
2
3
4
5
6
7
8
9
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// 打印“six times three is 18”

在上例中,创建了一个 TimesTable 实例,用来表示整数 3 的乘法表。数值 3 被传递给结构体的构造函数,作为实例成员 multiplier 的值。

你可以通过下标访问 threeTimesTable 实例,例如上面演示的 threeTimesTable[6]。这条语句查询了 3 的乘法表中的第六个元素,返回 36 倍即 18

注意

TimesTable 例子基于一个固定的数学公式,对 threeTimesTable[someIndex] 进行赋值操作并不合适,因此下标定义为只读的。

下标用法

下标的确切含义取决于使用场景。下标通常作为访问集合,列表或序列中元素的快捷方式。你可以针对自己特定的类或结构体的功能来自由地以最恰当的方式实现下标。

例如,Swift 的 Dictionary 类型实现下标用于对其实例中储存的值进行存取操作。为字典设值时,在下标中使用和字典的键类型相同的键,并把一个和字典的值类型相同的值赋给这个下标:

1
2
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2

上例定义一个名为 numberOfLegs 的变量,并用一个包含三对键值的字典字面量初始化它。numberOfLegs 字典的类型被推断为 [String: Int]。字典创建完成后,该例子通过下标将 String 类型的键 birdInt 类型的值 2 添加到字典中。

更多关于 Dictionary 下标的信息请参考 读取和修改字典

注意

Swift 的 Dictionary 类型的下标接受并返回可选类型的值。上例中的 numberOfLegs 字典通过下标返回的是一个 Int? 或者说“可选的 int”。Dictionary 类型之所以如此实现下标,是因为不是每个键都有个对应的值,同时这也提供了一种通过键删除对应值的方式,只需将键对应的值赋值为 nil 即可。

下标选项

下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用可变参数,并且可以提供默认参数数值,但是不能使用输入输出参数。

一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标,这就是下标的重载

虽然接受单一入参的下标是最常见的,但也可以根据情况定义接受多个入参的下标。例如下例定义了一个 Matrix 结构体,用于表示一个 Double 类型的二维矩阵。Matrix 结构体的下标接受两个整型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Matrix {
let rows: Int, columns: Int
var grid: [Double]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(repeating: 0.0, count: rows * columns)
}
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Double {
get {
assert(indexIsValid(row: row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValid(row: row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
}
}

Matrix 提供了一个接受两个入参的构造方法,入参分别是 rowscolumns,创建了一个足够容纳 rows * columnsDouble 类型的值的数组。通过传入数组长度和初始值 0.0 到数组的构造器,将矩阵中每个位置的值初始化为 0.0。关于数组的这种构造方法请参考 创建一个带有默认值的数组

你可以通过传入合适的 rowcolumn 的数量来构造一个新的 Matrix 实例:

1
var matrix = Matrix(rows: 2, columns: 2)

上例中创建了一个 Matrix 实例来表示两行两列的矩阵。该 Matrix 实例的 grid 数组按照从左上到右下的阅读顺序将矩阵扁平化存储:

rowcolumn 的值传入下标来为矩阵设值,下标的入参使用逗号分隔:

1
2
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2

上面两条语句分别调用下标的 setter 将矩阵右上角位置(即 row0column1 的位置)的值设置为 1.5,将矩阵左下角位置(即 row1column0 的位置)的值设置为 3.2

Matrix 下标的 getter 和 setter 中都含有断言,用来检查下标入参 rowcolumn 的值是否有效。为了方便进行断言,Matrix 包含了一个名为 indexIsValid(row:column:) 的便利方法,用来检查入参 rowcolumn 的值是否在矩阵范围内:

1
2
3
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}

断言在下标越界时触发:

1
2
let someValue = matrix[2, 2]
// 断言将会触发,因为 [2, 2] 已经超过了 matrix 的范围

类型下标

正如上节所述,实例下标是在特定类型的一个实例上调用的下标。你也可以定义一种在这个类型本身上调用的下标。这种下标的类型被称作类型下标。你可以通过在 subscript 关键字之前写下 static 关键字的方式来表示一个类型下标。类可以使用 class 关键字来允许子类重写父类中对那个下标的实现。下面的例子展示了如何定义和调用一个类型下标:

1
2
3
4
5
6
7
8
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
static subscript(n: Int) -> Planet {
return Planet(rawValue: n)!
}
}
let mars = Planet[4]
print(mars)

留言與分享

swift方法

分類 编程语言, swift

方法

方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法;实例方法为给定类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与 Objective-C 中的类方法(class methods)相似。

结构体和枚举能够定义方法是 Swift 与 C/Objective-C 的主要区别之一。在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活地在你创建的类型(类/结构体/枚举)上定义方法。

实例方法(Instance Methods)

实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致,详情参见 函数

实例方法要写在它所属的类型的前后大括号之间。实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。实例方法只能被它所属的类的某个特定实例调用。实例方法不能脱离于现存的实例而被调用。

下面的例子,定义一个很简单的 Counter 类,Counter 能被用来对一个动作发生的次数进行计数:

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}

Counter 类定义了三个实例方法:

  • increment 让计数器按一递增;
  • increment(by: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。

和调用属性一样,用点语法(dot syntax)调用实例方法:

1
2
3
4
5
6
7
8
let counter = Counter()
// 初始计数值是0
counter.increment()
// 计数值现在是1
counter.increment(by: 5)
// 计数值现在是6
counter.reset()
// 计数值现在是0

函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用),详情参见 指定外部参数名。方法参数也一样,因为方法就是函数,只是这个函数与某个类型相关联了。

self 属性

类型的每一个实例都有一个隐含属性叫做 selfself 完全等同于该实例本身。你可以在一个实例的实例方法中使用这个隐含的 self 属性来引用当前实例。

上面例子中的 increment 方法还可以这样写:

1
2
3
func increment() {
self.count += 1
}

实际上,你不必在你的代码里面经常写 self。不论何时,只要在一个方法中使用一个已知的属性或者方法名称,如果你没有明确地写 self,Swift 假定你是指当前实例的属性或者方法。这种假定在上面的 Counter 中已经示范了:Counter 中的三个实例方法中都使用的是 count(而不是 self.count)。

使用这条规则的主要场景是实例方法的某个参数名称与实例的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更严格的方式。这时你可以使用 self 属性来区分参数名称和属性名称。

下面的例子中,self 消除方法参数 x 和实例属性 x 之间的歧义:

1
2
3
4
5
6
7
8
9
10
11
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
print("This point is to the right of the line where x == 1.0")
}
// 打印“This point is to the right of the line where x == 1.0”

如果不使用 self 前缀,Swift会认为 x 的两个用法都引用了名为 x 的方法参数。

在实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择 可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的 self 属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

要使用 可变方法,将关键字 mutating 放到方法的 func 关键字之前就可以了:

1
2
3
4
5
6
7
8
9
10
11
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 打印“The point is now at (3.0, 4.0)”

上面的 Point 结构体定义了一个可变方法 moveBy(x:y :) 来移动 Point 实例到给定的位置。该方法被调用时修改了这个点,而不是返回一个新的点。方法定义时加上了 mutating 关键字,从而允许修改属性。

注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性,详情参见 常量结构体的存储属性

1
2
3
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// 这里将会报告一个错误

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。上面 Point 的例子可以用下面的方式改写:

1
2
3
4
5
6
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}

新版的可变方法 moveBy(x:y:) 创建了一个新的结构体实例,它的 x 和 y 的值都被设定为目标值。调用这个版本的方法和调用上个版本的最终结果是一样的。

枚举的可变方法可以把 self 设置为同一枚举类型中不同的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum TriStateSwitch {
case off, low, high
mutating func next() {
switch self {
case .off:
self = .low
case .low:
self = .high
case .high:
self = .off
}
}
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight 现在等于 .high
ovenLight.next()
// ovenLight 现在等于 .off

上面的例子中定义了一个三态切换的枚举。每次调用 next() 方法时,开关在不同的电源状态(off, low, high)之间循环切换。

类型方法

实例方法是被某个类型的实例调用的方法。你也可以定义在类型本身上调用的方法,这种方法就叫做类型方法。在方法的 func 关键字之前加上关键字 static,来指定类型方法。类还可以用关键字 class 来指定,从而允许子类重写父类该方法的实现。

注意

在 Objective-C 中,你只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。

类型方法和实例方法一样用点语法调用。但是,你是在类型上调用这个方法,而不是在实例上调用。下面是如何在 SomeClass 类上调用类型方法的例子:

1
2
3
4
5
6
class SomeClass {
class func someTypeMethod() {
// 在这里实现类型方法
}
}
SomeClass.someTypeMethod()

在类型方法的方法体(body)中,self 属性指向这个类型本身,而不是类型的某个实例。这意味着你可以用 self 来消除类型属性和类型方法参数之间的歧义(类似于我们在前面处理实例属性和实例方法参数时做的那样)。

一般来说,在类型方法的方法体中,任何未限定的方法和属性名称,可以被本类中其他的类型方法和类型属性引用。一个类型方法可以直接通过类型方法的名称调用本类中的其它类型方法,而无需在方法名称前面加上类型名称。类似地,在结构体和枚举中,也能够直接通过类型属性的名称访问本类中的类型属性,而不需要前面加上类型名称。

下面的例子定义了一个名为 LevelTracker 结构体。它监测玩家的游戏发展情况(游戏的不同层次或阶段)。这是一个单人游戏,但也可以存储多个玩家在同一设备上的游戏信息。

游戏初始时,所有的游戏等级(除了等级 1)都被锁定。每次有玩家完成一个等级,这个等级就对这个设备上的所有玩家解锁。LevelTracker 结构体用类型属性和方法监测游戏的哪个等级已经被解锁。它还监测每个玩家的当前等级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct LevelTracker {
static var highestUnlockedLevel = 1
var currentLevel = 1

static func unlock(_ level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}

static func isUnlocked(_ level: Int) -> Bool {
return level <= highestUnlockedLevel
}

@discardableResult
mutating func advance(to level: Int) -> Bool {
if LevelTracker.isUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}

LevelTracker 监测玩家已解锁的最高等级。这个值被存储在类型属性 highestUnlockedLevel 中。

LevelTracker 还定义了两个类型方法与 highestUnlockedLevel 配合工作。第一个类型方法是 unlock(_:),一旦新等级被解锁,它会更新 highestUnlockedLevel 的值。第二个类型方法是 isUnlocked(_:),如果某个给定的等级已经被解锁,它将返回 true。(注意,尽管我们没有使用类似 LevelTracker.highestUnlockedLevel 的写法,这个类型方法还是能够访问类型属性 highestUnlockedLevel

除了类型属性和类型方法,LevelTracker 还监测每个玩家的进度。它用实例属性 currentLevel 来监测每个玩家当前的等级。

为了便于管理 currentLevel 属性,LevelTracker 定义了实例方法 advance(to:)。这个方法会在更新 currentLevel 之前检查所请求的新等级是否已经解锁。advance(to:) 方法返回布尔值以指示是否能够设置 currentLevel。因为允许在调用 advance(to:) 时候忽略返回值,不会产生编译警告,所以函数被标注为 @discardableResult 属性,更多关于属性信息,请参考 特性章节。

下面,Player 类使用 LevelTracker 来监测和更新每个玩家的发展进度:

1
2
3
4
5
6
7
8
9
10
11
class Player {
var tracker = LevelTracker()
let playerName: String
func complete(level: Int) {
LevelTracker.unlock(level + 1)
tracker.advance(to: level + 1)
}
init(name: String) {
playerName = name
}
}

Player 类创建一个新的 LevelTracker 实例来监测这个用户的进度。它提供了 complete(level:) 方法,一旦玩家完成某个指定等级就调用它。这个方法为所有玩家解锁下一等级,并且将当前玩家的进度更新为下一等级。(我们忽略了 advance(to:) 返回的布尔值,因为之前调用 LevelTracker.unlock(_:) 时就知道了这个等级已经被解锁了)。

你还可以为一个新的玩家创建一个 Player 的实例,然后看这个玩家完成等级一时发生了什么:

1
2
3
4
var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// 打印“highest unlocked level is now 2”

如果你创建了第二个玩家,并尝试让他开始一个没有被任何玩家解锁的等级,那么试图设置玩家当前等级将会失败:

1
2
3
4
5
6
7
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}
// 打印“level 6 has not yet been unlocked”

留言與分享

swift属性

分類 编程语言, swift

属性

属性将值与特定的类、结构体或枚举关联。存储属性会将常量和变量存储为实例的一部分,而计算属性则是直接计算(而不是存储)值。计算属性可以用于类、结构体和枚举,而存储属性只能用于类和结构体。

存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接与类型本身关联,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发自定义的操作。属性观察器可以添加到类本身定义的存储属性上,也可以添加到从父类继承的属性上。

存储属性

简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。

可以在定义存储属性的时候指定默认值,请参考 默认构造器 一节。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值,请参考 构造过程中常量属性的修改 一节。

下面的例子定义了一个名为 FixedLengthRange 的结构体,该结构体用于描述整数的区间,且这个范围值在被创建后不能被修改。

1
2
3
4
5
6
7
8
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间表示整数 0,1,2
rangeOfThreeItems.firstValue = 6
// 该区间现在表示整数 6,7,8

FixedLengthRange 的实例包含一个名为 firstValue 的变量存储属性和一个名为 length 的常量存储属性。在上面的例子中,length 在创建实例的时候被初始化,且之后无法修改它的值,因为它是一个常量存储属性。

常量结构体实例的存储属性

如果创建了一个结构体实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使被声明为可变属性也不行:

1
2
3
4
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 该区间表示整数 0,1,2,3
rangeOfFourItems.firstValue = 6
// 尽管 firstValue 是个可变属性,但这里还是会报错

因为 rangeOfFourItems 被声明成了常量(用 let 关键字),所以即使 firstValue 是一个可变属性,也无法再修改它了。

这种行为是由于结构体属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。

属于引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性。

延时加载存储属性

延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延时加载存储属性。

注意

必须将延时加载属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。

当属性的值依赖于一些外部因素且这些外部因素只有在构造过程结束之后才会知道的时候,延时加载属性就会很有用。或者当获得属性的值因为需要复杂或者大量的计算,而需要采用需要的时候再计算的方式,延时加载属性也会很有用。

下面的例子使用了延时加载存储属性来避免复杂类中不必要的初始化工作。例子中定义了 DataImporterDataManager 两个类,下面是部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DataImporter {
/*
DataImporter 是一个负责将外部文件中的数据导入的类。
这个类的初始化会消耗不少时间。
*/
var fileName = "data.txt"
// 这里会提供数据导入功能
}

class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 这里会提供数据管理功能
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 实例的 importer 属性还没有被创建

DataManager 类包含一个名为 data 的存储属性,初始值是一个空的字符串数组。这里没有给出全部代码,只需知道 DataManager 类的目的是管理和提供对这个字符串数组的访问即可。

DataManager 的一个功能是从文件中导入数据。这个功能由 DataImporter 类提供,DataImporter 完成初始化需要消耗不少时间:因为它的实例在初始化时可能需要打开文件并读取文件中的内容到内存中。

DataManager 管理数据时也可能不从文件中导入数据。所以当 DataManager 的实例被创建时,没必要创建一个 DataImporter 的实例,更明智的做法是第一次用到 DataImporter 的时候才去创建它。

由于使用了 lazyDataImporter 的实例 importer 属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName 时:

1
2
3
print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了
// 输出“data.txt”

注意

如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

存储属性和实例变量

如果您有过 Objective-C 经验,应该知道 Objective-C 为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为一个备份存储将变量值赋值给属性。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的备份存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——作为类型定义的一部分,都定义在一个地方。

计算属性

除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印“square.origin is now at (10.0, 10.0)”

这个例子定义了 3 个结构体来描述几何形状:

  • Point 封装了一个 (x, y) 的坐标
  • Size 封装了一个 width 和一个 height
  • Rect 表示一个有原点和尺寸的矩形

Rect 也提供了一个名为 center 的计算属性。一个 Rect 的中心点可以从 origin(原点)和 size(大小)算出,所以不需要将中心点以 Point 类型的值来保存。Rect 的计算属性 center 提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。

上述例子中创建了一个名为 squareRect 实例,初始值原点是 (0, 0),宽度高度都是 10。如下图中蓝色正方形所示。

squarecenter 属性可以通过点运算符(square.center)来访问,这会调用该属性的 getter 来获取它的值。跟直接返回已经存在的值不同,getter 实际上通过计算然后返回一个新的 Point 来表示 square 的中心点。如代码所示,它正确返回了中心点 (5, 5)

center 属性之后被设置了一个新的值 (15, 15),表示向右上方移动正方形到如下图橙色正方形所示的位置。设置属性 center 的值会调用它的 setter 来修改属性 originxy 的值,从而实现移动正方形到新的位置。

Computed Properties sample

简化 Setter 声明

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。下面是使用了简化 setter 声明的 Rect 结构体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}

简化 Getter 声明

如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 Rect 结构体,用到了简化的 getter 和 setter 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}

在 getter 中忽略 return 与在函数中忽略 return 的规则相同,请参考 隐式返回的函数

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

注意

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

只读计算属性的声明可以去掉 get 关键字和花括号:

1
2
3
4
5
6
7
8
9
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 打印“the volume of fourByFiveByTwo is 40.0”

这个例子定义了一个名为 Cuboid 的结构体,表示三维空间的立方体,包含 widthheightdepth 属性。结构体还有一个名为 volume 的只读计算属性用来返回立方体的体积。为 volume 提供 setter 毫无意义,因为无法确定如何修改 widthheightdepth 三者的值来匹配新的 volume。然而,Cuboid 提供一个只读计算属性来让外部用户直接获取体积是很有用的。

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

你可以为除了延时加载存储属性之外的其他存储属性添加属性观察器,你也可以在子类中通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。你不必为非重写的计算属性添加属性观察器,因为你可以直接通过它的 setter 监控和响应值的变化。属性重写请参考 重写

可以为属性添加其中一个或两个观察器:

  • willSet 在新的值被设置之前调用
  • didSet 在新的值被设置之后调用

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。

同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

注意

在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSetdidSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。

有关构造器代理的更多信息,请参考 值类型的构造器代理类的构造器代理

下面是一个 willSetdidSet 实际运用的例子,其中定义了一个名为 StepCounter 的类,用来统计一个人步行时的总步数。这个类可以跟计步器或其他日常锻炼的统计装置的输入数据配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("将 totalSteps 的值设置为 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 将 totalSteps 的值设置为 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 将 totalSteps 的值设置为 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 将 totalSteps 的值设置为 896
// 增加了 536 步

StepCounter 类定义了一个叫 totalStepsInt 类型的属性。它是一个存储属性,包含 willSetdidSet 观察器。

totalSteps 被设置新值的时候,它的 willSetdidSet 观察器都会被调用,即使新值和当前值完全相同时也会被调用。

例子中的 willSet 观察器将表示新值的参数自定义为 newTotalSteps,这个观察器只是简单的将新的值输出。

didSet 观察器在 totalSteps 的值改变后被调用,它把新值和旧值进行对比,如果总步数增加了,就输出一个消息表示增加了多少步。didSet 没有为旧值提供自定义名称,所以默认值 oldValue 表示旧值的参数名。

注意

如果将带有观察器的属性通过 in-out 方式传入函数,willSetdidSet 也会调用。这是因为 in-out 参数采用了拷入拷出内存模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。关于 in-out 参数详细的介绍,请参考 输入输出参数

全局变量和局部变量

计算属性和观察属性所描述的功能也可以用于全局变量局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。

前面章节提到的全局或局部变量都属于存储型变量,跟存储属性类似,它为特定类型的值提供存储空间,并允许读取和写入。

另外,在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。

注意

全局的常量或变量都是延迟计算的,跟 延时加载存储属性 相似,不同的地方在于,全局的常量或变量不需要标记 lazy 修饰符。

局部范围的常量和变量从不延迟计算。

类型属性

实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。

你也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性

类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。

存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。

注意

跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。

存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。

类型属性语法

在 C 或 Objective-C 中,与某个类型关联的静态常量和静态变量,是作为 global(全局)静态变量定义的。但是在 Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。

使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。下面的例子演示了存储型和计算型类型属性的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}

注意

例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟计算型实例属性的语法相同。

获取和设置类型属性的值

跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。比如:

1
2
3
4
5
6
7
8
9
print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”

下面的例子定义了一个结构体,使用两个存储型类型属性来表示两个声道的音量,每个声道具有 010 之间的整数音量。

下图展示了如何把两个声道结合来模拟立体声的音量。当声道的音量是 0,没有一个灯会亮;当声道的音量是 10,所有灯点亮。本图中,左声道的音量是 9,右声道的音量是 7

Static Properties VUMeter

上面所描述的声道模型使用 AudioChannel 结构体的实例来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 将当前音量限制在阈值之内
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存储当前音量作为新的最大输入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}

AudioChannel 结构定义了 2 个存储型类型属性来实现上述功能。第一个是 thresholdLevel,表示音量的最大上限阈值,它是一个值为 10 的常量,对所有实例都可见,如果音量高于 10,则取最大上限值 10(见后面描述)。

第二个类型属性是变量存储型属性 maxInputLevelForAllChannels,它用来表示所有 AudioChannel 实例的最大输入音量,初始值是 0

AudioChannel 也定义了一个名为 currentLevel 的存储型实例属性,表示当前声道现在的音量,取值为 010

属性 currentLevel 包含 didSet 属性观察器来检查每次设置后的属性值,它做如下两个检查:

  • 如果 currentLevel 的新值大于允许的阈值 thresholdLevel,属性观察器将 currentLevel 的值限定为阈值 thresholdLevel
  • 如果修正后的 currentLevel 值大于静态类型属性 maxInputLevelForAllChannels 的值,属性观察器就将新值保存在 maxInputLevelForAllChannels 中。

注意

在第一个检查过程中,didSet 属性观察器将 currentLevel 设置成了不同的值,但这不会造成属性观察器被再次调用。

可以使用结构体 AudioChannel 创建两个声道 leftChannelrightChannel,用以表示立体声系统的音量:

1
2
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果将左声道的 currentLevel 设置成 7,类型属性 maxInputLevelForAllChannels 也会更新成 7

1
2
3
4
5
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 输出“7”
print(AudioChannel.maxInputLevelForAllChannels)
// 输出“7”

如果试图将右声道的 currentLevel 设置成 11,它会被修正到最大值 10,同时 maxInputLevelForAllChannels 的值也会更新到 10

1
2
3
4
5
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 输出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 输出“10”

留言與分享

swift类和结构

分類 编程语言, swift

结构体和类

结构体作为一种通用而又灵活的结构,成为了人们构建代码的基础。你可以使用定义常量、变量和函数的语法,为你的结构体和类定义属性、添加方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义的结构体和类的接口与实现代码分别创建文件。你只需在单一的文件中定义一个结构体或者类,系统将会自动生成面向其它代码的外部接口。

注意

通常一个的实例被称为对象。然而相比其他语言,Swift 中结构体和类的功能更加相近,本章中所讨论的大部分功能都可以用在结构体或者类上。因此,这里会使用实例这个更通用的术语。

结构体和类对比

Swift 中结构体和类有很多共同点。两者都可以:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能

更多信息请参见 属性方法下标构造过程扩展协议

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

更多信息请参见 继承类型转换析构过程自动引用计数

类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。更多详细的比较参见 在结构和类之间进行选择

类型定义的语法

结构体和类有着相似的定义方式。你通过 struct 关键字引入结构体,通过 class 关键字引入类,并将它们的具体定义放在一对大括号中:

1
2
3
4
5
6
struct SomeStructure {
// 在这里定义结构体
}
class SomeClass {
// 在这里定义类
}

注意

每当你定义一个新的结构体或者类时,你都是定义了一个新的 Swift 类型。请使用 UpperCamelCase 这种方式来命名类型(如这里的 SomeClassSomeStructure),以便符合标准 Swift 类型的大写命名风格(如 StringIntBool)。请使用 lowerCamelCase 这种方式来命名属性和方法(如 framerateincrementCount),以便和类型名区分。

以下是定义结构体和定义类的示例:

1
2
3
4
5
6
7
8
9
10
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}

在上面的示例中定义了一个名为 Resolution 的结构体,用来描述基于像素的分辨率。这个结构体包含了名为 widthheight 的两个存储属性。存储属性是与结构体或者类绑定的,并存储在其中的常量或变量。当这两个属性被初始化为整数 0 的时候,它们会被推断为 Int 类型。

在上面的示例还定义了一个名为 VideoMode 的类,用来描述视频显示器的某个特定视频模式。这个类包含了四个可变的存储属性。第一个, resolution,被初始化为一个新的 Resolution 结构体的实例,属性类型被推断为 Resolution。新 VideoMode 实例同时还会初始化其它三个属性,它们分别是初始值为 falseinterlaced(意为“非隔行视频”),初始值为 0.0frameRate,以及值为可选 Stringname。因为 name 是一个可选类型,它会被自动赋予一个默认值 nil,意为“没有 name 值”。

结构体和类的实例

Resolution 结构体和 VideoMode 类的定义仅描述了什么是 ResolutionVideoMode。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为此,你需要创建结构体或者类的一个实例。

创建结构体和类实例的语法非常相似:

1
2
let someResolution = Resolution()
let someVideoMode = VideoMode()

结构体和类都使用构造器语法来创建新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如 Resolution()VideoMode()。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。构造过程 章节会对类和结构体的初始化进行更详细的讨论。

属性访问

你可以通过使用点语法访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者以点号(.)分隔,不带空格:

1
2
print("The width of someResolution is \(someResolution.width)")
// 打印 "The width of someResolution is 0"

在上面的例子中,someResolution.width 引用 someResolutionwidth 属性,返回 width 的初始值 0

你也可以访问子属性,如 VideoModeresolution 属性的 width 属性:

1
2
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is 0"

你也可以使用点语法为可变属性赋值:

1
2
3
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is now 1280"

结构体类型的成员逐一构造器

所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:

1
let vga = Resolution(width: 640, height: 480)

与结构体不同,类实例没有默认的成员逐一构造器。构造过程 章节会对构造器进行更详细的讨论。

结构体和枚举是值类型

值类型是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝

在之前的章节中,你已经大量使用了值类型。实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。

Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。

注意

标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。

请看下面这个示例,其使用了上一个示例中的 Resolution 结构体:

1
2
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

在以上示例中,声明了一个名为 hd 的常量,其值为一个初始化为全高清视频分辨率(1920 像素宽,1080 像素高)的 Resolution 实例。

然后示例中又声明了一个名为 cinema 的变量,并将 hd 赋值给它。因为 Resolution 是一个结构体,所以会先创建一个现有实例的副本,然后将副本赋值给 cinema 。尽管 hdcinema 有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。

下面,为了符合数码影院放映的需求(2048 像素宽,1080 像素高),cinemawidth 属性被修改为稍微宽一点的 2K 标准:

1
cinema.width = 2048

查看 cinemawidth 属性,它的值确实改为了 2048

1
2
print("cinema is now  \(cinema.width) pixels wide")
// 打印 "cinema is now 2048 pixels wide"

然而,初始的 hd 实例中 width 属性还是 1920

1
2
print("hd is still \(hd.width) pixels wide")
// 打印 "hd is still 1920 pixels wide"

hd 赋值给 cinema 时,hd 中所存储的会拷贝到新的 cinema 实例中。结果就是两个完全独立的实例包含了相同的数值。由于两者相互独立,因此将 cinemawidth 修改为 2048 并不会影响 hd 中的 width 的值,如下图所示:

sharedStateStruct_2x

枚举也遵循相同的行为准则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum CompassPoint {
case north, south, east, west
mutating func turnNorth() {
self = .north
}
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// 打印 "The current direction is north"
// 打印 "The remembered direction is west"

rememberedDirection 被赋予了 currentDirection 的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改 currentDirection 的值并不影响 rememberedDirection 所储存的原始值的拷贝。

类是引用类型

与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。

请看下面这个示例,其使用了之前定义的 VideoMode 类:

1
2
3
4
5
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

以上示例中,声明了一个名为 tenEighty 的常量,并让其引用一个 VideoMode 类的新实例。它的视频模式(video mode)被赋值为之前创建的 HD 分辨率(1920*1080)的一个拷贝。然后将它设置为隔行视频,名字设为 “1080i”,并将帧率设置为 25.0 帧每秒。

接下来,将 tenEighty 赋值给一个名为 alsoTenEighty 的新常量,并修改 alsoTenEighty 的帧率:

1
2
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

因为类是引用类型,所以 tenEightalsoTenEight 实际上引用的是同一个 VideoMode 实例。换句话说,它们是同一个实例的两种叫法,如下图所示:

sharedStateClass_2x

通过查看 tenEightyframeRate 属性,可以看到它正确地显示了底层的 VideoMode 实例的新帧率 30.0

1
2
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 打印 "The frameRate property of theEighty is now 30.0"

这个例子也显示了为何引用类型更加难以理解。如果 tenEightyalsoTenEighty 在你代码中的位置相距很远,那么就很难找到所有修改视频模式的地方。无论在哪使用 tenEighty,你都要考虑使用 alsoTenEighty 的代码,反之亦然。相反,值类型就更容易理解了,因为你的源码中与同一个值交互的代码都很近。

需要注意的是 tenEightyalsoTenEighty 被声明为常量而不是变量。然而你依然可以改变 tenEighty.frameRatealsoTenEighty.frameRate,这是因为 tenEightyalsoTenEighty 这两个常量的值并未改变。它们并不“存储”这个 VideoMode 实例,而仅仅是对 VideoMode 实例的引用。所以,改变的是底层 VideoMode 实例的 frameRate 属性,而不是指向 VideoMode 的常量引用的值。

恒等运算符

因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)

判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:

  • 相同(===
  • 不相同(!==

使用这两个运算符检测两个常量或者变量是否引用了同一个实例:

1
2
3
4
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// 打印 "tenEighty and alsoTenEighty refer to the same VideoMode instance."

请注意,“相同”(用三个等号表示,===)与“等于”(用两个等号表示,==)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。

当在定义你的自定义结构体和类的时候,你有义务来决定判定两个实例“相等”的标准。在章节 等价操作符 中将会详细介绍实现自定义 == 和 !== 运算符的流程。

指针

如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(*)来表明你在创建一个引用。相反,Swift 中引用的定义方式与其它的常量或变量的一样。如果需要直接与指针交互,你可以使用标准库提供的指针和缓冲区类型 —— 参见 手动管理内存

留言與分享

swift枚举

分類 编程语言, swift

枚举

枚举为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。

如果你熟悉 C 语言,你会知道在 C 语言中,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串、字符,或是一个整型值或浮点数。

此外,枚举成员可以指定任意类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值。

在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。

想了解更多相关信息,请参见 属性方法构造过程扩展协议

枚举语法

使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

1
2
3
enum SomeEnumeration {
// 枚举定义放在这里
}

下面是用枚举表示指南针四个方向的例子:

1
2
3
4
5
6
enum CompassPoint {
case north
case south
case east
case west
}

枚举中定义的值(如 northsoutheastwest)是这个枚举的成员值(或成员)。你可以使用 case 关键字来定义一个新的枚举成员值。

注意

与 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的 CompassPoint 例子中,northsoutheastwest 不会被隐式地赋值为 0123。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的 CompassPoint 类型。

多个成员值可以出现在同一行上,用逗号隔开:

1
2
3
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

每个枚举定义了一个全新的类型。像 Swift 中其他类型一样,它们的名字(例如 CompassPointPlanet)以一个大写字母开头。给枚举类型起一个单数名字而不是复数名字,以便于:

1
var directionToHead = CompassPoint.west

directionToHead 的类型可以在它被 CompassPoint 的某个值初始化时推断出来。一旦 directionToHead 被声明为 CompassPoint 类型,你可以使用更简短的点语法将其设置为另一个 CompassPoint 的值:

1
directionToHead = .east

directionToHead 的类型已知时,再次为其赋值可以省略枚举类型名。在使用具有显式类型的枚举值时,这种写法让代码具有更好的可读性。

使用 Switch 语句匹配枚举值

你可以使用 switch 语句匹配单个枚举值:

1
2
3
4
5
6
7
8
9
10
11
12
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 打印“Watch out for penguins”

你可以这样理解这段代码:

“判断 directionToHead 的值。当它等于 .north,打印 “Lots of planets have a north”。当它等于 .south,打印 “Watch out for penguins”。”

……以此类推。

正如在 控制流 中介绍的那样,在判断一个枚举类型的值时,switch 语句必须穷举所有情况。如果忽略了 .west 这种情况,上面那段代码将无法通过编译,因为它没有考虑到 CompassPoint 的全部成员。强制穷举确保了枚举成员不会被意外遗漏。

当不需要匹配每个枚举成员的时候,你可以提供一个 default 分支来涵盖所有未明确处理的枚举成员:

1
2
3
4
5
6
7
8
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// 打印“Mostly harmless”

枚举成员的遍历

在一些情况下,你会需要得到一个包含枚举所有成员的集合。可以通过如下代码实现:

令枚举遵循 CaseIterable 协议。Swift 会生成一个 allCases 属性,用于表示一个包含枚举所有成员的集合。下面是一个例子:

1
2
3
4
5
6
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// 打印“3 beverages available”

在前面的例子中,通过 Beverage.allCases 可以访问到包含 Beverage 枚举所有成员的集合。allCases 的使用方法和其它一般集合一样——集合中的元素是枚举类型的实例,所以在上面的情况中,这些元素是 Beverage 值。在前面的例子中,统计了总共有多少个枚举成员。而在下面的例子中,则使用 for 循环来遍历所有枚举成员。

1
2
3
4
5
6
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice

在前面的例子中,使用的语法表明这个枚举遵循 CaseIterable 协议。想了解 protocols 相关信息,请参见 协议

关联值

枚举语法那一小节的例子演示了如何定义和分类枚举的成员。你可以为 Planet.earth 设置一个常量或者变量,并在赋值之后查看这个值。然而,有时候把其他类型的值和成员值一起存储起来会很有用。这额外的信息称为关联值,并且你每次在代码中使用该枚举成员时,还可以修改这个关联值。

你可以定义 Swift 枚举来存储任意类型的关联值,如果需要的话,每个枚举成员的关联值类型可以各不相同。枚举的这种特性跟其他语言中的可识别联合(discriminated unions),标签联合(tagged unions),或者变体(variants)相似。

例如,假设一个库存跟踪系统需要利用两种不同类型的条形码来跟踪商品。有些商品上标有使用 09 的数字的 UPC 格式的一维条形码。每一个条形码都有一个代表数字系统的数字,该数字后接五位代表厂商代码的数字,接下来是五位代表“产品代码”的数字。最后一个数字是检查位,用来验证代码是否被正确扫描:

其他商品上标有 QR 码格式的二维码,它可以使用任何 ISO 8859-1 字符,并且可以编码一个最多拥有 2,953 个字符的字符串:

这便于库存跟踪系统用包含四个整型值的元组存储 UPC 码,以及用任意长度的字符串储存 QR 码。

在 Swift 中,使用如下方式定义表示两种商品条形码的枚举:

1
2
3
4
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}

以上代码可以这么理解:

“定义一个名为 Barcode 的枚举类型,它的一个成员值是具有 (Int,Int,Int,Int) 类型关联值的 upc,另一个成员值是具有 String 类型关联值的 qrCode。”

这个定义不提供任何 IntString 类型的关联值,它只是定义了,当 Barcode 常量和变量等于 Barcode.upcBarcode.qrCode 时,可以存储的关联值的类型。

然后你可以使用任意一种条形码类型创建新的条形码,例如:

1
var productBarcode = Barcode.upc(8, 85909, 51226, 3)

上面的例子创建了一个名为 productBarcode 的变量,并将 Barcode.upc 赋值给它,关联的元组值为 (8, 85909, 51226, 3)

同一个商品可以被分配一个不同类型的条形码,例如:

1
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这时,原始的 Barcode.upc 和其整数关联值被新的 Barcode.qrCode 和其字符串关联值所替代。Barcode 类型的常量和变量可以存储一个 .upc 或者一个 .qrCode(连同它们的关联值),但是在同一时间只能存储这两个值中的一个。

你可以使用一个 switch 语句来检查不同的条形码类型,和之前使用 Switch 语句来匹配枚举值的例子一样。然而,这一次,关联值可以被提取出来作为 switch 语句的一部分。你可以在 switch 的 case 分支代码中提取每个关联值作为一个常量(用 let 前缀)或者作为一个变量(用 var 前缀)来使用:

1
2
3
4
5
6
7
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
// 打印“QR code: ABCDEFGHIJKLMNOP.”

如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 let 或者 var

1
2
3
4
5
6
7
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// 打印“QR code: ABCDEFGHIJKLMNOP.”

原始值

关联值 小节的条形码例子中,演示了如何声明存储不同类型关联值的枚举成员。作为关联值的替代选择,枚举成员可以被默认值(称为原始值)预填充,这些原始值的类型必须相同。

这是一个使用 ASCII 码作为原始值的枚举:

1
2
3
4
5
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}

枚举类型 ASCIIControlCharacter 的原始值类型被定义为 Character,并设置了一些比较常见的 ASCII 控制字符。Character 的描述详见 字符串和字符 部分。

原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。

注意

原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。

原始值的隐式赋值

在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增 1。如果第一个枚举成员没有设置原始值,其原始值将为 0

下面的枚举是对之前 Planet 这个枚举的一个细化,利用整型的原始值来表示每个行星在太阳系中的顺序:

1
2
3
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

在上面的例子中,Plant.mercury 的显式原始值为 1Planet.venus 的隐式原始值为 2,依次类推。

当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。

下面的例子是 CompassPoint 枚举的细化,使用字符串类型的原始值来表示各个方向的名称:

1
2
3
enum CompassPoint: String {
case north, south, east, west
}

上面例子中,CompassPoint.south 拥有隐式原始值 south,依次类推。

使用枚举成员的 rawValue 属性可以访问该枚举成员的原始值:

1
2
3
4
5
let earthsOrder = Planet.earth.rawValue
// earthsOrder 值为 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection 值为 "west"

使用原始值初始化枚举实例

如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 rawValue 的参数,参数类型即为原始值类型,返回值则是枚举成员或 nil。你可以使用这个初始化方法来创建一个新的枚举实例。

这个例子利用原始值 7 创建了枚举成员 Uranus

1
2
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 类型为 Planet? 值为 Planet.uranus

然而,并非所有 Int 值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个可选的枚举成员。在上面的例子中,possiblePlanetPlanet? 类型,或者说“可选的 Planet”。

注意

原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。更多信息请参见 可失败构造器

如果你试图寻找一个位置为 11 的行星,通过原始值构造器返回的可选 Planet 值将是 nil

1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
// 打印“There isn't a planet at position 11”

这个例子使用了可选绑定(optional binding),试图通过原始值 11 来访问一个行星。if let somePlanet = Planet(rawValue: 11) 语句创建了一个可选 Planet,如果可选 Planet 的值存在,就会赋值给 somePlanet。在这个例子中,无法检索到位置为 11 的行星,所以 else 分支被执行。

递归枚举

递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect 来表示该成员可递归。

例如,下面的例子中,枚举类型存储了简单的算术表达式:

1
2
3
4
5
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

你也可以在枚举类型开头加上 indirect 关键字来表明它的所有成员都是可递归的:

1
2
3
4
5
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}

上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员 additionmultiplication 的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。例如,表达式 (5 + 4) * 2,乘号右边是一个数字,左边则是另一个表达式。因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。下面的代码展示了使用 ArithmeticExpression 这个递归枚举创建表达式 (5 + 4) * 2

1
2
3
4
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}

print(evaluate(product))
// 打印“18”

该函数如果遇到纯数字,就直接返回该数字的值。如果遇到的是加法或乘法运算,则分别计算左边表达式和右边表达式的值,然后相加或相乘。

留言與分享

swift闭包

分類 编程语言, swift

闭包

闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。

闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。

注意

如果你不熟悉捕获(capturing)这个概念也不用担心,在 值捕获 章节有它更详细的介绍。

函数 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:

  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:

  • 利用上下文推断参数和返回值类型
  • 隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字
  • 参数名称缩写
  • 尾随闭包语法

闭包表达式

嵌套函数 作为复杂函数的一部分时,它自包含代码块式的定义和命名形式在使用上带来了方便。当然,编写未完整声明和没有函数名的类函数结构代码是很有用的,尤其是在编码中涉及到函数作为参数的那些方法时。

闭包表达式是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。下面通过对 sorted(by:) 这一个案例的多次迭代改进来展示这个过程,每次迭代都使用了更加简明的方式描述了相同功能。。

排序方法

Swift 标准库提供了名为 sorted(by:) 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,sorted(by:) 方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被 sorted(by:) 方法修改。

下面的闭包表达式示例使用 sorted(by:) 方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:

1
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:) 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:) 方法的参数传入:

1
2
3
4
5
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串(s1)大于第二个字符串(s2),backward(_:_:) 函数会返回 true,表示在新的数组中 s1 应该出现在 s2 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 "B" 大于字母 "A" ,字符串 "Tom" 大于字符串 "Tim"。该闭包将进行字母逆序排序,"Barry" 将会排在 "Alex" 之前。

然而,以这种方式来编写一个实际上很简单的表达式(a > b),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好地构造一个内联排序闭包。

闭包表达式语法

闭包表达式语法有如下的一般形式:

1
2
3
{ (parameters) -> return type in
statements
}

闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。

下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:

1
2
3
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})

需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。

闭包的函数体部分由关键字 in 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:

1
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。

根据上下文推断类型

因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:) 方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String)Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:

1
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 sorted(by:) 方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。

单表达式闭包的隐式返回

单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

1
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。

参数名称缩写

Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推。

如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

1
reversedNames = names.sorted(by: { $0 > $1 } )

在这个例子中,$0$1 表示闭包中第一个和第二个 String 类型的参数。

运算符方法

实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:

1
reversedNames = names.sorted(by: >)

更多关于运算符方法的内容请查看 运算符方法

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}

// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}

闭包表达式语法 上章节中的字符串排序闭包可以作为尾随包的形式改写在 sorted(by:) 方法圆括号的外面:

1
reversedNames = names.sorted() { $0 > $1 }

如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:

1
reversedNames = names.sorted { $0 > $1 }

当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。举例来说,Swift 的 Array 类型有一个 map(_:) 方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。

当提供给数组的闭包应用于每个数组元素后,map(_:) 方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。

下例介绍了如何在 map(_:) 方法中使用尾随闭包将 Int 类型数组 [16, 58, 510] 转换为包含对应 String 类型的值的数组 ["OneSix", "FiveEight", "FiveOneZero"]

1
2
3
4
5
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

如上代码创建了一个整型数位和它们英文版本名字相映射的字典。同时还定义了一个准备转换为字符串数组的整型数组。

你现在可以通过传递一个尾随闭包给 numbers 数组的 map(_:) 方法来创建对应的字符串版本数组:

1
2
3
4
5
6
7
8
9
10
11
12
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]

map(_:) 为数组中每一个元素调用了一次闭包表达式。你不需要指定闭包的输入参数 number 的类型,因为可以通过要映射的数组类型进行推断。

在该例中,局部变量 number 的值由闭包中的 number 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 String,以表明存储映射值的新数组类型为 String

闭包表达式在每次被调用的时候创建了一个叫做 output 的字符串并返回。其使用求余运算符(number % 10)计算最后一位数字并利用 digitNames 字典获取所映射的字符串。这个闭包能够用于创建任意正整数的字符串表示。

注意

字典 digitNames 下标后跟着一个叹号(!),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定 number % 10 总是 digitNames 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 String 类型的值。

digitNames 字典中获取的字符串被添加到 output前部,逆序建立了一个字符串版本的数字。(在表达式 number % 10 中,如果 number16,则返回 658 返回 8510 返回 0。)

number 变量之后除以 10。因为其是整数,在计算过程中未除尽部分被忽略。因此 16 变成了 158 变成了 5510 变成了 51

整个过程重复进行,直到 number /= 100,这时闭包会将字符串 output 返回,而 map(_:) 方法则会将字符串添加到映射数组中。

在上面的例子中,通过尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在 map(_:) 方法的括号内。

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

举个例子,这有一个叫做 makeIncrementer 的函数,其包含了一个叫做 incrementer 的嵌套函数。嵌套函数 incrementer() 从上下文中捕获了两个值,runningTotalamount。捕获这些值之后,makeIncrementerincrementer 作为闭包返回。每次调用 incrementer 时,其会以 amount 作为增量增加 runningTotal 的值。

1
2
3
4
5
6
7
8
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}

makeIncrementer 返回类型为 () -> Int。这意味着其返回的是一个函数,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个 Int 类型的值。关于函数返回其他函数的内容,请查看 函数类型作为返回类型

makeIncrementer(forIncrement:) 函数定义了一个初始值为 0 的整型变量 runningTotal,用来存储当前总计数值。该值为 incrementer 的返回值。

makeIncrementer(forIncrement:) 有一个 Int 类型的参数,其外部参数名为 forIncrement,内部参数名为 amount,该参数表示每次 incrementer 被调用时 runningTotal 将要增加的量。makeIncrementer 函数还定义了一个嵌套函数 incrementer,用来执行实际的增加操作。该函数简单地使 runningTotal 增加 amount,并将其返回。

如果我们单独考虑嵌套函数 incrementer(),会发现它有些不同寻常:

1
2
3
4
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}

incrementer() 函数并没有任何参数,但是在函数体内访问了 runningTotalamount 变量。这是因为它从外围函数捕获了 runningTotalamount 变量的引用。捕获引用保证了 runningTotalamount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在。

注意

为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。

Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。

下面是一个使用 makeIncrementer 的例子:

1
let incrementByTen = makeIncrementer(forIncrement: 10)

该例子定义了一个叫做 incrementByTen 的常量,该常量指向一个每次调用会将其 runningTotal 变量增加 10incrementer 函数。调用这个函数多次可以得到以下结果:

1
2
3
4
5
6
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30

如果你创建了另一个 incrementer,它会有属于自己的引用,指向一个全新、独立的 runningTotal 变量:

1
2
3
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值为7

再次调用原来的 incrementByTen 会继续增加它自己的 runningTotal 变量,该变量和 incrementBySeven 中捕获的变量没有任何联系:

1
2
incrementByTen()
// 返回的值为40

注意

如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用。更多信息,请参考 闭包引起的循环强引用

闭包是引用类型

上面的例子中,incrementBySevenincrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型

无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen 是一个常量,而并非闭包内容本身。

这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

1
2
3
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:

1
2
3
4
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping,就会得到一个编译错误。

将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self。相对的,传递到 someFunctionWithNonescapingClosure(_:) 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}

class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出“200”

completionHandlers.first?()
print(instance.x)
// 打印出“100”

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false 时被计算求值。

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出“5”

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出“5”

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出“4”

尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider 的类型不是 String,而是 () -> String,一个没有参数且返回值为 String 的函数。

将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

1
2
3
4
5
6
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出“Now serving Alex!”

上面的 serve(customer:) 函数接受一个返回顾客名字的显式的闭包。下面这个版本的 serve(customer:) 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure 来接收一个自动闭包。现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。

1
2
3
4
5
6
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

注意

过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure@escaping 属性。@escaping 属性的讲解见上面的 逃逸闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 打印“Collected 2 closures.”
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印“Now serving Barry!”
// 打印“Now serving Daniella!”

在上面的代码中,collectCustomerProviders(_:) 函数并没有调用传入的 customerProvider 闭包,而是将闭包追加到了 customerProviders 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,customerProvider 参数必须允许“逃逸”出函数作用域。

留言與分享

作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau