Swift 学习笔记(四)
116.使用可选链式调用代替强制展开
通过在想调用的属性、方法、或下标的可选值(optional value)后面放一个问号(?),可以定义一个可选链。这一点很像在可选值后面放一个叹号(!)来强制展开它的值。它们的主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制展开将会触发运行时错误。
为了反映可选链式调用可以在空值(nil)上调用的事实,不论这个调用的属性、方法及下标返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来判断你的可选链式调用是否调用成功,如果调用有返回值则说明调用成功,返回nil则说明调用失败。
特别地,可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是Int类型,则会变为Int?类型。
下面几段代码将解释可选链式调用和强制展开的不同。
首先定义两个类Person和Residence:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence有一个Int类型的属性numberOfRooms,其默认值为1。Person具有一个可选的residence属性,其类型为Residence?。
如果创建一个新的Person实例,因为它的residence属性是可选的,john属性将初始化为nil:
let john = Person()
如果使用叹号(!)强制展开获得这个john的residence属性中的numberOfRooms值,会触发运行时错误,因为这时residence没有可以展开的值:
let roomCount = john.residence!.numberOfRooms
// 这会引发运行时错误
john.residence为非nil值的时候,上面的调用会成功,并且把roomCount设置为Int类型的房间数量。正如上面提到的,当residence为nil的时候上面这段代码会触发运行时错误。
可选链式调用提供了另一种访问numberOfRooms的方式,使用问号(?)来替代原来的叹号(!):
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”。如上例所示,当residence为nil的时候,可选的Int将会为nil,表明无法访问numberOfRooms。访问成功时,可选的Int值会通过可选绑定展开,并赋值给非可选类型的roomCount常量。
要注意的是,即使numberOfRooms是非可选的Int时,这一点也成立。只要使用可选链式调用就意味着numberOfRooms会返回一个Int?而不是Int。
可以将一个Residence的实例赋给john.residence,这样它就不再是nil了:
john.residence = Residence()
john.residence现在包含一个实际的Residence实例,而不再是nil。如果你试图使用先前的可选链式调用访问numberOfRooms,它现在将返回值为1的Int?类型的值:
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).”
117.为可选链式调用定义模型类
通过使用可选链式调用可以调用多层属性、方法和下标。这样可以在复杂的模型中向下访问各种子属性,并且判断能否访问子属性的属性、方法或下标。
下面这段代码定义了四个模型类,这些例子包括多层可选链式调用。为了方便说明,在Person和Residence的基础上增加了Room类和Address类,以及相关的属性、方法以及下标。
Person类的定义基本保持不变:
class Person {
var residence: Residence?
}
Residence类比之前复杂些,增加了一个名为rooms的变量属性,该属性被初始化为[Room]类型的空数组:
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,以及一个用于将该属性设置为适当的房间名的初始化函数:
class Room {
let name: String
init(name: String) { self.name = name }
}
最后一个类是Address,这个类有三个String?类型的可选属性。buildingName以及buildingNumber属性分别表示某个大厦的名称和号码,第三个属性street表示大厦所在街道的名称:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName != nil {
return buildingName
} else if buildingNumber != nil && street != nil {
return "\(buildingNumber) \(street)"
} else {
return nil
}
}
}
Address类提供了buildingIdentifier()方法,返回值为String?。 如果buildingName有值则返回buildingName。或者,如果buildingNumber和street均有值则返回buildingNumber。否则,返回nil。
通过可选链式调用访问属性
正如使用可选链式调用代替强制展开中所述,可以通过可选链式调用在一个可选值上访问它的属性,并判断访问是否成功。
下面的代码创建了一个Person实例,然后像之前一样,尝试访问numberOfRooms属性:
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.residence为nil,所以这个可选链式调用依旧会像先前一样失败。
还可以通过可选链式调用来设置属性值:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
在这个例子中,通过john.residence来设定address属性也会失败,因为john.residence当前为nil。
上面代码中的赋值过程是可选链式调用的一部分,这意味着可选链式调用失败时,等号右侧的代码不会被执行。对于上面的代码来说,很难验证这一点,因为像这样赋值一个常量没有任何副作用。下面的代码完成了同样的事情,但是它使用一个函数来创建Address实例,然后将该实例返回用于赋值。该函数会在返回前打印“Function was called”,这使你能验证等号右侧的代码是否被执行。
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值,如下所示:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
这个方法没有返回值。然而,没有返回值的方法具有隐式的返回类型Void,如无返回值函数中所述。这意味着没有返回值的方法也会返回(),或者说空的元组。
如果在可选值上通过可选链式调用来调用这个方法,该方法的返回类型会是Void?,而不是Void,因为通过可选链式调用得到的返回值都是可选的。这样我们就可以使用if语句来判断能否成功调用printNumberOfRooms()方法,即使方法本身没有定义返回值。通过判断返回值是否为nil可以判断调用是否成功:
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属性赋值,即使residence为nil。通过可选链式调用给属性赋值会返回Void?,通过判断返回值是否为nil就可以知道赋值是否成功:
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.residence为nil,所以下标调用失败了:
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是可选值。
类似的,可以通过下标,用可选链式调用来赋值:
john.residence?[0] = Room(name: "Bathroom")
这次赋值同样会失败,因为residence目前是nil。
如果你创建一个Residence实例,并为其rooms数组添加一些Room实例,然后将Residence实例赋值给john.residence,那就可以通过可选链和下标来访问数组中的元素:
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类型的键的下标,可以在下标的结尾括号后面放一个问号来在其可选返回值上进行可选链式调用:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0]++
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都是可选值:
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属性:
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包含一个有效的Residence实例,所以对john.residence的address属性赋值将会成功。
在方法的可选返回值上进行可选链式调用
上面的例子展示了如何在一个可选值上通过可选链式调用来获取它的属性值。我们还可以在一个可选值上通过可选链式调用来调用方法,并且可以根据需要继续在方法的可选返回值上进行可选链式调用。
在下面的例子中,通过可选链式调用来调用Address的buildingIdentifier()方法。这个方法返回String?类型的值。如上所述,通过可选链式调用来调用该方法,最终的返回值依旧会是String?类型:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// 打印 “John's building identifier is The Larches.”
如果要在该方法的返回值上进行可选链式调用,在方法的圆括号后面加上问号即可:
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()方法的可选返回值上进行可选链式调用,而不是方法本身。
118.表示并抛出错误
在 Swift 中,错误用符合ErrorType协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。
Swift 的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。例如,你可以这样表示在一个游戏中操作自动贩卖机时可能会出现的错误状态:
enum VendingMachineError: ErrorType {
case InvalidSelection //选择无效
case InsufficientFunds(coinsNeeded: Int) //金额不足
case OutOfStock //缺货
}
抛出一个错误可以让你表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用throws关键字。例如,下面的代码抛出一个错误,提示贩卖机还需要5个硬币:
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
119.用 throwing 函数传递错误
为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数列表之后加上throws关键字。一个标有throws关键字的函数被称作throwing 函数。如果这个函数指明了返回值类型,throws关键词需要写在箭头(->)的前面。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
一个 throwing 函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。
注意
只有 throwing 函数可以传递错误。任何在某个非 throwing 函数内部抛出的错误只能在函数内部处理。
下面的例子中,VendingMechine类有一个vend(itemNamed:)方法,如果请求的物品不存在、缺货或者花费超过了投入金额,该方法就会抛出一个相应的VendingMachineError:
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func dispenseSnack(snack: String) {
print("Dispensing \(snack)")
}
func vend(itemNamed name: String) throws {
guard var item = inventory[name] else {
throw VendingMachineError.InvalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.OutOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
--item.count
inventory[name] = item
dispenseSnack(name)
}
}
在vend(itemNamed:)方法的实现中使用了guard语句来提前退出方法,确保在购买某个物品所需的条件中,有任一条件不满足时,能提前退出方法并抛出相应的错误。由于throw语句会立即退出方法,所以物品只有在所有条件都满足时才会被售出。
因为vend(itemNamed:)方法会传递出它抛出的任何错误,在你的代码中调用此方法的地方,必须要么直接处理这些错误——使用do-catch语句,try?或try!;要么继续将这些错误传递下去。例如下面例子中,buyFavoriteSnack(_:vendingMachine:)同样是一个 throwing 函数,任何由vend(itemNamed:)方法抛出的错误会一直被传递到buyFavoriteSnack(_:vendingMachine:)函数被调用的地方。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
上例中,buyFavoriteSnack(_:vendingMachine:)函数会查找某人最喜欢的零食,并通过调用vend(itemNamed:)方法来尝试为他们购买。因为vend(itemNamed:)方法能抛出错误,所以在调用的它时候在它前面加了try关键字。
120用 Do-Catch 处理错误
可以使用一个do-catch语句运行一段闭包代码来处理错误。如果在do子句中的代码抛出了一个错误,这个错误会与catch子句做匹配,从而决定哪条子句能处理它。
下面是do-catch语句的一般形式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
}
在catch后面写一个匹配模式来表明这个子句能处理什么样的错误。如果一条catch子句没有指定匹配模式,那么这条子句可以匹配任何错误,并且把错误绑定到一个名字为error的局部常量。关于模式匹配的更多信息请参考 模式。
catch子句不必将do子句中的代码所抛出的每一个可能的错误都作处理。如果所有catch子句都未处理错误,错误就会传递到周围的作用域。然而,错误还是必须要被某个周围的作用域处理的——要么是一个外围的do-catch错误处理语句,要么是一个 throwing 函数的内部。举例来说,下面的代码处理了VendingMachineError枚举类型的全部枚举值,但是所有其它的错误就必须由它周围的作用域处理:
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
}
// 打印 “Insufficient funds. Please insert an additional 2 coins.”
上面的例子中,buyFavoriteSnack(_:vendingMachine:)函数在一个try表达式中调用,因为它能抛出错误。如果错误被抛出,相应的执行会马上转移到catch子句中,并判断这个错误是否要被继续传递下去。如果没有错误抛出,do子句中余下的语句就会被执行。
121.将错误转换成可选值
可以使用try?通过将错误转换成一个可选值来处理错误。如果在评估try?表达式时一个错误被抛出,那么表达式的值就是nil。例如下面代码中的x和y具有相同的值:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
如果someThrowingFunction()抛出一个错误,x和y的值是nil。否则x和y的值就是该函数的返回值。注意,无论someThrowingFunction()的返回值类型是什么类型,x和y都是这个类型的可选类型。例子中此函数返回一个整型,所以x和y是可选整型。
如果你想对所有的错误都采用同样的方式来处理,用try?就可以让你写出简洁的错误处理代码。例如,下面的代码用几种方式来获取数据,如果所有方式都失败了则返回nil:
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
禁用错误传递
有时你知道某个 throwing 函数实际上在运行时是不会抛出错误的,在这种情况下,你可以在表达式前面写try!来禁用错误传递,这会把调用包装在一个断言不会有错误抛出的运行时断言中。如果实际上抛出了错误,你会得到一个运行时错误。
例如,下面的代码使用了loadImage(_:)函数,该函数从给定的路径加载图片资源,如果图片无法载入则抛出一个错误。在这种情况下,因为图片是和应用绑定的,运行时不会有错误抛出,所以适合禁用错误传递:
let photo = try! loadImage("./Resources/John Appleseed.jpg")
122.指定清理操作
可以使用defer语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,还是由于诸如return或者break的语句。例如,你可以用defer语句来确保文件描述符得以关闭,以及手动分配的内存得以释放。
defer语句将代码的执行延迟到当前的作用域退出之前。该语句由defer关键字和要被延迟执行的语句组成。延迟执行的语句不能包含任何控制转移语句,例如break或是return语句,或是抛出一个错误。延迟执行的操作会按照它们被指定时的顺序的相反顺序执行——也就是说,第一条defer语句中的代码会在第二条defer语句中的代码被执行之后才执行,以此类推。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// 处理文件。
}
// close(file) 会在这里被调用,即作用域的最后。
}
}
上面的代码使用一条defer语句来确保open(_:)函数有一个相应的对close(_:)函数的调用。
注意
即使没有涉及到错误处理,你也可以使用defer语句。
123.类型转换(Type Casting)
类型转换在 Swift 中使用 is 和 as 操作符实现。这两个操作符提供了一种简单达意的方式去检查值的类型或者转换它的类型。
定义一个类层次作为例子
你可以将类型转换用在类和子类的层次结构上,检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。下面的三个代码段定义了一个类层次和一个包含了这些类实例的数组,作为类型转换的例子。
第一个代码片段定义了一个新的基类 MediaItem。这个类为任何出现在数字媒体库的媒体项提供基础功能。特别的,它声明了一个 String 类型的 name 属性,和一个 init(name:) 初始化器。(假定所有的媒体项都有个名称。)
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
下一个代码段定义了 MediaItem 的两个子类。第一个子类 Movie 封装了与电影相关的额外信息,在父类(或者说基类)的基础上增加了一个 director(导演)属性,和相应的初始化器。第二个子类 Song,在父类的基础上增加了一个artist(艺术家)属性,和相应的初始化器:
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
最后一个代码段创建了一个数组常量 library,包含两个 Movie 实例和三个 Song 实例。library 的类型是在它被初始化时根据它数组中所包含的内容推断来的。Swift 的类型检测器能够推断出 Movie 和 Song 有共同的父类MediaItem,所以它推断出 [MediaItem] 类作为 library 的类型:
let library = [
Movie(name: "Casablanca", director: "Michael Curtiz"),
Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
Movie(name: "Citizen Kane", director: "Orson Welles"),
Song(name: "The One And Only", artist: "Chesney Hawkes"),
Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// 数组 library 的类型被推断为 [MediaItem]
在幕后 library 里存储的媒体项依然是 Movie 和 Song 类型的。但是,若你迭代它,依次取出的实例会是MediaItem 类型的,而不是 Movie 和 Song 类型。为了让它们作为原本的类型工作,你需要检查它们的类型或者向下转换它们到其它类型,就像下面描述的一样。
检查类型(Checking Type)
用类型检查操作符(is)来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false。
下面的例子定义了两个变量,movieCount 和 songCount,用来计算数组 library 中 Movie 和 Song 类型的实例数量:
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie {
++movieCount
} else if item is Song {
++songCount
}
}
print("Media library contains \(movieCount) movies and \(songCount) songs")
// 打印 “Media library contains 2 movies and 3 songs”
示例迭代了数组 library 中的所有项。每一次,for-in 循环设置 item 为数组中的下一个 MediaItem。
若当前 MediaItem 是一个 Movie 类型的实例,item is Movie 返回 true,否则返回 false。同样的,item is Song 检查 item 是否为 Song 类型的实例。在循环结束后,movieCount 和 songCount 的值就是被找到的属于各自类型的实例的数量。
向下转型(Downcasting)
某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,你可以尝试向下转到它的子类型,用类型转换操作符(as? 或 as!)。
因为向下转型可能会失败,类型转型操作符带有两种不同形式。条件形式(conditional form)as? 返回一个你试图向下转成的类型的可选值(optional value)。强制形式 as! 把试图向下转型和强制解包(force-unwraps)转换结果结合为一个操作。
当你不确定向下转型可以成功时,用类型转换的条件形式(as?)。条件形式的类型转换总是返回一个可选值(optional value),并且若下转是不可能的,可选值将是 nil。这使你能够检查向下转型是否成功。
只有你可以确定向下转型一定会成功时,才使用强制形式(as!)。当你试图向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。
下面的例子,迭代了 library 里的每一个 MediaItem,并打印出适当的描述。要这样做,item 需要真正作为Movie 或 Song 的类型来使用,而不仅仅是作为 MediaItem。为了能够在描述中使用 Movie 或 Song 的 director或 artist 属性,这是必要的。
在这个示例中,数组中的每一个 item 可能是 Movie 或 Song。事前你不知道每个 item 的真实类型,所以这里使用条件形式的类型转换(as?)去检查循环里的每次下转:
for item in library {
if let movie = item as? Movie {
print("Movie: '\(movie.name)', dir. \(movie.director)")
} else if let song = item as? Song {
print("Song: '\(song.name)', by \(song.artist)")
}
}
// Movie: 'Casablanca', dir. Michael Curtiz
// Song: 'Blue Suede Shoes', by Elvis Presley
// Movie: 'Citizen Kane', dir. Orson Welles
// Song: 'The One And Only', by Chesney Hawkes
// Song: 'Never Gonna Give You Up', by Rick Astley
示例首先试图将 item 下转为 Movie。因为 item 是一个 MediaItem 类型的实例,它可能是一个 Movie;同样,它也可能是一个 Song,或者仅仅是基类 MediaItem。因为不确定,as? 形式在试图下转时将返回一个可选值。item as? Movie 的返回值是 Movie? 或者说“可选 Movie”。
当向下转型为 Movie 应用在两个 Song 实例时将会失败。为了处理这种情况,上面的例子使用了可选绑定(optional binding)来检查可选 Movie 真的包含一个值(这个是为了判断下转是否成功。)可选绑定是这样写的“if let movie = item as? Movie”,可以这样解读:
“尝试将 item 转为 Movie 类型。若成功,设置一个新的临时常量 movie 来存储返回的可选 Movie 中的值”
若向下转型成功,然后 movie 的属性将用于打印一个 Movie 实例的描述,包括它的导演的名字 director。相似的原理被用来检测 Song 实例,当 Song 被找到时则打印它的描述(包含 artist 的名字)。
注意
转换没有真的改变实例或它的值。根本的实例保持不变;只是简单地把它作为它被转换成的类型来使用。
Any 和 AnyObject 的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
- AnyObject 可以表示任何类类型的实例。
- Any 可以表示任何类型,包括函数类型。
注意
只有当你确实需要它们的行为和功能时才使用 Any 和 AnyObject。在你的代码里使用你期望的明确类型总是更好的。
AnyObject 类型
当在工作中使用 Cocoa APIs 时,我们经常会接收到一个 [AnyObject] 类型的数组,或者说“一个任意类型对象的数组”。这是因为 Objective-C 没有明确的类型化数组。但是,你常常可以从 API 提供的信息来确定数组中对象的类型。
译者注
这段文档似乎没有及时更新,从 Xcode 7 和 Swift 2.0 开始,由于 Objective-C 引入了轻量泛型,集合类型已经可以类型化了,在 Swift 中使用 Cocoa API 也越来越少遇到 AnyObject 类型了。详情请参阅 Lightweight Generics 和 Collection Classes。
在这些情况下,你可以使用强制形式的类型转换(as)来下转数组中的每一项到比 AnyObject 更明确的类型,不需要可选解包(optional unwrapping)。
下面的示例定义了一个 [AnyObject] 类型的数组并填入三个 Movie 类型的实例:
let someObjects: [AnyObject] = [
Movie(name: "2001: A Space Odyssey", director: "Stanley Kubrick"),
Movie(name: "Moon", director: "Duncan Jones"),
Movie(name: "Alien", director: "Ridley Scott")
]
因为知道这个数组只包含 Movie 实例,你可以直接用(as!)下转并解包到非可选的 Movie 类型:
for object in someObjects {
let movie = object as! Movie
print("Movie: '\(movie.name)', dir. \(movie.director)")
}
// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones
// Movie: 'Alien', dir. Ridley Scott
为了变为一个更简短的形式,下转 someObjects 数组为 [Movie] 类型而不是下转数组中的每一项:
for movie in someObjects as! [Movie] {
print("Movie: '\(movie.name)', dir. \(movie.director)")
}
// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones
// Movie: 'Alien', dir. Ridley Scott
Any 类型
这里有个示例,使用 Any 类型来和混合的不同类型一起工作,包括函数类型和非类类型。它创建了一个可以存储 Any类型的数组 things:
var things = [Any]()
things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })
things 数组包含两个 Int 值,两个 Double 值,一个 String 值,一个元组 (Double, Double),一个Movie实例“Ghostbusters”,以及一个接受 String 值并返回另一个 String 值的闭包表达式。
你可以在 switch 表达式的 case 中使用 is 和 as 操作符来找出只知道是 Any 或 AnyObject 类型的常量或变量的具体类型。下面的示例迭代 things 数组中的每一项,并用 switch 语句查找每一项的类型。有几个 switch 语句的 case 绑定它们匹配到的值到一个指定类型的常量,从而可以打印这些值:
for thing in things {
switch thing {
case 0 as Int:
print("zero as an Int")
case 0 as Double:
print("zero as a Double")
case let someInt as Int:
print("an integer value of \(someInt)")
case let someDouble as Double where someDouble > 0:
print("a positive double value of \(someDouble)")
case is Double:
print("some other double value that I don't want to print")
case let someString as String:
print("a string value of \"\(someString)\"")
case let (x, y) as (Double, Double):
print("an (x, y) point at \(x), \(y)")
case let movie as Movie:
print("a movie called '\(movie.name)', dir. \(movie.director)")
case let stringConverter as String -> String:
print(stringConverter("Michael"))
default:
print("something else")
}
}
// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called 'Ghostbusters', dir. Ivan Reitman
// Hello, Michael
124.嵌套类型实践
下面这个例子定义了一个结构体BlackjackCard(二十一点),用来模拟BlackjackCard中的扑克牌点数。BlackjackCard结构体包含两个嵌套定义的枚举类型Suit和Rank。
在BlackjackCard中,Ace牌可以表示1或者11,Ace牌的这一特征通过一个嵌套在Rank枚举中的结构体Values来表示:
struct BlackjackCard {
// 嵌套的 Suit 枚举
enum Suit: Character {
case Spades = "♠", Hearts = "♡", Diamonds = "♢", Clubs = "♣"
}
// 嵌套的 Rank 枚举
enum Rank: Int {
case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King, Ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .Ace:
return Values(first: 1, second: 11)
case .Jack, .Queen, .King:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}
// BlackjackCard 的属性和方法
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
Suit枚举用来描述扑克牌的四种花色,并用一个Character类型的原始值表示花色符号。
Rank枚举用来描述扑克牌从Ace~10,以及J、Q、K,这13种牌,并用一个Int类型的原始值表示牌的面值。(这个Int类型的原始值未用于Ace、J、Q、K这4种牌。)
如上所述,Rank枚举在内部定义了一个嵌套结构体Values。结构体Values中定义了两个属性,用于反映只有Ace有两个数值,其余牌都只有一个数值:
- first的类型为Int
- second的类型为Int?,或者说“optional Int”
Rank还定义了一个计算型属性values,它将会返回一个Values结构体的实例。这个计算型属性会根据牌的面值,用适当的数值去初始化Values实例。对于J、Q、K、Ace这四种牌,会使用特殊数值。对于数字面值的牌,使用枚举实例的原始值。
BlackjackCard结构体拥有两个属性——rank与suit。它也同样定义了一个计算型属性description,description属性用rank和suit中的内容来构建对扑克牌名字和数值的描述。该属性使用可选绑定来检查可选类型second是否有值,若有值,则在原有的描述中增加对second的描述。
因为BlackjackCard是一个没有自定义构造器的结构体,在结构体的逐一成员构造器中可知,结构体有默认的成员构造器,所以你可以用默认的构造器去初始化新常量theAceOfSpades:
let theAceOfSpades = BlackjackCard(rank: .Ace, suit: .Spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// 打印 “theAceOfSpades: suit is ♠, value is 1 or 11”
尽管Rank和Suit嵌套在BlackjackCard中,但它们的类型仍可从上下文中推断出来,所以在初始化实例时能够单独通过成员名称(.Ace和.Spades)引用枚举实例。在上面的例子中,description属性正确地反映了黑桃A牌具有1和11两个值。
引用嵌套类型
在外部引用嵌套类型时,在嵌套类型的类型名前加上其外部类型的类型名作为前缀:
let heartsSymbol = BlackjackCard.Suit.Hearts.rawValue
// 红心符号为 “♡”
对于上面这个例子,这样可以使Suit、Rank和Values的名字尽可能的短,因为它们的名字可以由定义它们的上下文来限定。
125.扩展就是为一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即 逆向建模 )。扩展和 Objective-C 中的分类类似。(与 Objective-C 不同的是,Swift 的扩展没有名字。)
Swift 中的扩展可以:
- 添加计算型属性和计算型类型属性
- 定义实例方法和类型方法
- 提供新的构造器
- 定义下标
- 定义和使用新的嵌套类型
- 使一个已有类型符合某个协议
在 Swift 中,你甚至可以对协议进行扩展,提供协议要求的实现,或者添加额外的功能,从而可以让符合协议的类型拥有这些功能。你可以从协议扩展获取更多的细节。
注意
扩展可以为一个类型添加新的功能,但是不能重写已有的功能。
扩展语法(Extension Syntax)
使用关键字 extension 来声明扩展:
extension SomeType {
// 为 SomeType 添加的新功能写到这里
}
可以通过扩展来扩展一个已有类型,使其采纳一个或多个协议。在这种情况下,无论是类还是结构体,协议名字的书写方式完全一样:
extension SomeType: SomeProtocol, AnotherProctocol {
// 协议实现写到这里
}
通过这种方式添加协议一致性的详细描述请参阅利用扩展添加协议一致性。
注意
如果你通过扩展为一个已有类型添加新功能,那么新功能对该类型的所有已有实例都是可用的,即使它们是在这个扩展定义之前创建的。
计算型属性(Computed Properties)
扩展可以为已有类型添加计算型实例属性和计算型类型属性。下面的例子为 Swift 的内建 Double 类型添加了五个计算型实例属性,从而提供与距离单位协作的基本支持:
extension Double {
var km: Double { return self * 1_000.0 }
var m : Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// 打印 “One inch is 0.0254 meters”
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// 打印 “Three feet is 0.914399970739201 meters”
这些计算型属性表达的含义是把一个 Double 值看作是某单位下的长度值。即使它们被实现为计算型属性,但这些属性的名字仍可紧接一个浮点型字面值,从而通过点语法来使用,并以此实现距离转换。
在上述例子中,Double 值 1.0 用来表示“1米”。这就是为什么计算型属性 m 返回 self,即表达式 1.m 被认为是计算 Double 值 1.0。
其它单位则需要一些单位换算。一千米等于 1,000 米,所以计算型属性 km 要把值乘以 1_000.00 来实现千米到米的单位换算。类似地,一米有 3.28024 英尺,所以计算型属性 ft 要把对应的 Double 值除以 3.28024 来实现英尺到米的单位换算。
这些属性是只读的计算型属性,为了更简洁,省略了 get 关键字。它们的返回值是 Double,而且可以用于所有接受Double 值的数学计算中:
let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// 打印 “A marathon is 42195.0 meters long”
注意
扩展可以添加新的计算型属性,但是不可以添加存储型属性,也不可以为已有属性添加属性观察器。
构造器(Initializers)
扩展可以为已有类型添加新的构造器。这可以让你扩展其它类型,将你自己的定制类型作为其构造器参数,或者提供该类型的原始实现中未提供的额外初始化选项。
扩展能为类添加新的便利构造器,但是它们不能为类添加新的指定构造器或析构器。指定构造器和析构器必须总是由原始的类实现来提供。
注意
如果你使用扩展为一个值类型添加构造器,且该值类型的原始实现中未定义任何定制的构造器时,你可以在扩展中的构造器里调用逐一成员构造器。如果该值类型为所有存储型属性提供了默认值,你还可以在扩展中的构造器里调用默认构造器。
正如在值类型的构造器代理中描述的,如果你把定制的构造器写在值类型的原始实现中,上述规则将不再适用。
下面的例子定义了一个用于描述几何矩形的结构体 Rect。这个例子同时定义了两个辅助结构体 Size 和 Point,它们都把 0.0 作为所有属性的默认值:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}
因为结构体 Rect 未提供定制的构造器,因此它会获得一个逐一成员构造器。又因为它为所有存储型属性提供了默认值,它又会获得一个默认构造器。详情请参阅默认构造器。这些构造器可以用于构造新的 Rect 实例:
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
你可以提供一个额外的接受指定中心点和大小的构造器来扩展 Rect 结构体:
extension Rect {
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)
}
}
这个新的构造器首先根据提供的 center 和 size 的值计算一个合适的原点。然后调用该结构体的逐一成员构造器init(origin:size:),该构造器将新的原点和大小的值保存到了相应的属性中:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect 的原点是 (2.5, 2.5),大小是 (3.0, 3.0)
注意
如果你使用扩展提供了一个新的构造器,你依旧有责任确保构造过程能够让实例完全初始化。
方法(Methods)
扩展可以为已有类型添加新的实例方法和类型方法。下面的例子为 Int 类型添加了一个名为 repetitions 的实例方法:
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
这个 repetitions(:_) 方法接受一个 () -> Void 类型的单参数,表示没有参数且没有返回值的函数。
定义该扩展之后,你就可以对任意整数调用 repetitions(_:) 方法,将闭包中的任务执行整数对应的次数:
3.repetitions({
print("Hello!")
})
// Hello!
// Hello!
// Hello!
可以使用尾随闭包让调用更加简洁:
3.repetitions {
print("Goodbye!")
}
// Goodbye!
// Goodbye!
// Goodbye!
可变实例方法(Mutating Instance Methods)
通过扩展添加的实例方法也可以修改该实例本身。结构体和枚举类型中修改 self 或其属性的方法必须将该实例方法标注为 mutating,正如来自原始实现的可变方法一样。
下面的例子为 Swift 的 Int 类型添加了一个名为 square 的可变方法,用于计算原始值的平方值:
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt 的值现在是 9
下标(Subscripts)
扩展可以为已有类型添加新下标。这个例子为 Swift 内建类型 Int 添加了一个整型下标。该下标 [n] 返回十进制数字从右向左数的第 n 个数字:
- 123456789[0] 返回 9
- 123456789[1] 返回 8
……以此类推。
extension Int {
subscript(var digitIndex: Int) -> Int {
var decimalBase = 1
while digitIndex > 0 {
decimalBase *= 10
--digitIndex
}
return (self / decimalBase) % 10
}
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7
如果该 Int 值没有足够的位数,即下标越界,那么上述下标实现会返回 0,犹如在数字左边自动补 0:
746381295[9]
// 返回 0,即等同于:
0746381295[9]
嵌套类型(Nested Types)
扩展可以为已有的类、结构体和枚举添加新的嵌套类型:
extension Int {
enum Kind {
case Negative, Zero, Positive
}
var kind: Kind {
switch self {
case 0:
return .Zero
case let x where x > 0:
return .Positive
default:
return .Negative
}
}
}
该例子为 Int 添加了嵌套枚举。这个名为 Kind 的枚举表示特定整数的类型。具体来说,就是表示整数是正数、零或者负数。
这个例子还为 Int 添加了一个计算型实例属性,即 kind,用来根据整数返回适当的 Kind 枚举成员。
现在,这个嵌套枚举可以和任意 Int 值一起使用了:
func printIntegerKinds(numbers: [Int]) {
for number in numbers {
switch number.kind {
case .Negative:
print("- ", terminator: "")
case .Zero:
print("0 ", terminator: "")
case .Positive:
print("+ ", terminator: "")
}
}
print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// 打印 “+ + - 0 - 0 +”
函数 printIntegerKinds(_:) 接受一个 Int 数组,然后对该数组进行迭代。在每次迭代过程中,对当前整数的计算型属性 kind 的值进行评估,并打印出适当的描述。
注意
由于已知 number.kind 是 Int.Kind 类型,因此在 switch 语句中,Int.Kind 中的所有成员值都可以使用简写形式,例如使用 . Negative 而不是 Int.Kind.Negative。