86.复合 Cases
共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式匹配, 这个分支就被认为匹配。如果列表很长可以写作多行。如下:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 打印 "e is a vowel"
第一个分支匹配英文中五个小写元音字母。 类似的, 第二个分支匹配所有小写辅音字母。最后, default 分支 匹配其余的字母。
符合分支可以包含值绑定。 符合分支所有形式必须包括相同的值绑定集合, 每个绑定必须获取一个相同类型的值。这个确保, 无论复合分支哪个部分匹配, 分支代码总是访问一个绑定值,而且这个值总有相同类型。
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 打印 "On an axis, 9 from the origin"
上面的分支有两种形式: (let distance, 0) 匹配x轴上的点, (0, let distance) 匹配y轴上点。两个形式都绑定了一个距离值,两种形式下距离都是整形,这就意味着分支代码总是可以访问距离的值。
87.控制转移语句
控制转移语句改变代码执行的顺序, 通过转移控制从一块代码到另外一块代码。Swift 有五个控制转移语句:
1.continue
2.break
3.fallthrough
4.return
5.throw
88.Continue
continue 语句告诉当前循环停止,然后开始下一次循环。它说 “我在当前循环迭代” 没有离开当前的循环。
下面的例子从一个小写字符串移除所有的元音字母和空格,来创建一个谜语:
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
let charactersToRemove: [Character] = ["a", "e", "i", "o", "u", " "]
for character in puzzleInput.characters {
if charactersToRemove.contains(character) {
continue
} else {
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// 打印 "grtmndsthnklk"
上面的代码遇见元音或者空格就会调用 continue 关键字, 当前迭代立即结束然后进入下一次迭代。
89.Break
break 立即结束整个控制流语句的执行。break 语句可以用在一个 switch 语句或者循环语句,如果你想早点结束执行的话。
Break 在循环语句
用在循环语句时, break 立即结束循环,然后跳出循环所在的大括号。
Break 在Switch 语句
用在switch 语句时, break 终止 switch 语句的执行,然后跳出switch语句所在的大括号。
这种行为可以在switch语句中用来匹配或者忽略一个或者多个分支。 因为 Swift 的 switch 语句是详尽的而且不允许有空分支, 有时候需要故意匹配和忽略一个分支,为了让你的意图明显。整个分支写一个break就可以忽略。当分支匹配时, 分支里的break语句立即终止switch语句的执行。
下面的例子转换一个字符值,然后判断它在四种语言之一是否表示一个数字, 单个switch 分支覆盖了多个值。
let numberSymbol: Character = "三" // Chinese symbol for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// 打印 "The integer value of 三 is 3."
上面的例子判断 numberSymbol 是不是拉丁语, 阿拉伯语, 汉语, 或者泰语中的1到4。如果匹配到, switch中的一个分支会赋值给可选类型变量 possibleIntegerValue 一个合适的整数值。
在 switch 语句执行完成之后, 例子中使用可选绑定去判断一个值是否找到。possibleIntegerValue 变量是一个可选类型有一个初始值nil, 如果四个分支之一给possibleIntegerValue 设置了一个实际值,这个可选绑定就成功了。
因为不可能列出所有的可能字符, 一个默认的分支用来处理其他没匹配的字符。默认分支不需要做任何事, 所以它只有一个break语句。当默认分支被匹配后, break 语句就结束switch语句的执行, 代码开始从 if let 语句处继续执行。
90.Fallthrough
Swift 的 Switch 语句不会 fall through 到下一个分支。 相反, 只有第一个匹配到的分支语句执行完成,整个switch语句就完成了执行。不同的是, C 语言要求每个分支后面都要加上break 语句,防止 fallthrough. 明显Swift 更加简洁和安全。
如果你想要C语言那种效果的 fallthrough, 你可以选择在分支后面加上 fallthrough 关键字。 下面的例子使用fallthrough 创建一个数字的文字描述。
let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
description += " a prime number, and also"
fallthrough
default:
description += " an integer."
}
print(description)
// 打印 "The number 5 is a prime number, and also an integer."
上例声明了一个字符串变量 description ,然后赋了一个初始值。后面的函数在switch语句中用了这个值 integerToDescribe. 如果 integerToDescribe 的是列表中的一个素数, 函数就把一段文本添加到 description 的后面, 去备注说这个数字是素数。然后用了fallthrough 关键字进入默认的分支. 默认的分支添加一段额外的文本到 description 之后, 然后switch语句就结束了。
除非integerToDescribe 的值在已知素数列表里, 否则第一个分支就不会匹配。 因为没有其他分支, integerToDescribe 会被默认的分支匹配。
switch 语句执行完后, 使用 print(_:separator:terminator:) 函数打印description。 这个例子里, 数字5就是一个素数。
91.标签语句
在 Swift 里, 你可以在循环和条件语句种内嵌循环和条件语句,来创建更复杂的控制流结构。不过, 循环和条件语句都可以使用break 语句来提前结束执行。因此, 有时候这很有用,当你决定哪个循环或者条件语句要用break来终止执行。类似的, 如果你有多个嵌套的循环, 它也是有用的,可以明确哪个循环语句继续有效。
为了达到这些目的, 你可以用一个语句标签来标记一个循环或者条件语句。对于条件语句, 你可以使用带着break语句的语句标签来结束标签的语句。对于循环语句, 你可以用带着break或者continue的语句标签来结束或者继续标签语句的执行。
一个标签语句通过放置一个标签来指示,标签放在相同行作为语句的关键字。跟着是一个冒号。 这里有一个while循环语法的例子, 对于所有的循环和switch语句都是相当的规则:
label name: while condition {
statements
}
下面的例子使用带着标签的break和continue语句,这次游戏有了一个额外的规则: 想要赢,你就要登上方格25 如果掷骰子让你超过了方格25, 你必须重掷,直到投出能够登上方格25的数字为止。 游戏的棋盘和原来一样。
finalSquare, board, square, 和 diceRoll 初始化方式跟以前一样:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
这个版本的游戏使用了一个 while 循环和一个switch 语句来实现游戏的逻辑。 while 循环有一个语句标签 gameLoop 用来表示它是游戏的主要循环。
while 循环的条件是是 while square != finalSquare, 告诉你必须登上方格25。
gameLoop: while square != finalSquare {
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
switch square + diceRoll {
case finalSquare:
// diceRoll will move us to the final square, so the game is over
break gameLoop
case let newSquare where newSquare > finalSquare:
// diceRoll will move us beyond the final square, so roll again
continue gameLoop
default:
// this is a valid move, so find out its effect
square += diceRoll
square += board[square]
}
}
print("Game over!")
每次循环开始摇色子。 循环使用了一个switch语句来考虑移动的结果和是否允许移动,而不是立即用的玩家的位置:
如果摇色子使得玩家移动到最后的方格,那么游戏就结束了。break gameLoop 语句转移控制到整个循环的外面的第一行代码, 结束游戏。
如果摇色子使得玩家超出了最后的方格, 那么移动无效,玩家需要重新摇色子。continue gameLoop 语句结束当前的迭代然后开始下一次迭代。
其他所有情况, 摇色子都是有效的移动。 玩家往前移动 diceRoll 方格, 游戏逻辑检查所有的蛇与梯子。 循环结束, 控制返回条件判断,决定是否需要再来一次。
备注:如果break 语句不使用 gameLoop 标签, 它只会跳出switch 语句,而不会跳出while 语句。使用 gameLoop 标签清楚知道要终止哪个控制语句。
不是必须用 gameLoop 标签,当调用 continue gameLoop 来跳到下一次循环迭代。游戏里只有一个循环。continue 语句影响哪个循环是很清楚的。不过, 使用 gameLoop 标签也没有坏处。这样做是为了和break 标签使用一致,并且让逻辑更加轻易阅读和理解。
92.尽早退出
guard 语句, 很像 if 语句, 根据表达式布尔值执行语句。 使用guard 语句要求条件必须为真。和 if 语句不同, guard 语句总有一个else 字句—如果条件是假 else 会执行。
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// 打印 "Hello John!"
// 打印 "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// 打印 "Hello Jane!"
// 打印 "I hope the weather is nice in Cupertino."
如果 guard 语句的条件满足, 代码继续在guard 语句的大括号后执行。所有变量或者常量使用一个可选绑定赋值,它们作为条件的一部分。
如果条件没有满足, else 分支会执行。这个分支转移控制跳出guard 语句所在的代码块。可以使用控制转移语句 return, break, continue, 或者 throw, 或者也可以调用不返回的函数或者方法, 比如 fatalError(_:file:line:).
跟 if 语句做同样的判断比较,使用 guard 语句为了提高代码的可读性。它让你可以写通常不在else 中执行的代码, 同时让你保持代码,来处理违法的要求。
93.判断 API 可用性
Swift 支持判断 API 的有效性, 这样可以保证你不会在给定设备上使用不可用的API。
编译器使用SDK中的有效性信息来判断你代码中的所有API。 如果你使用不可用的API,Swift 会报一个编译错误。
在if或者guard语句中使用一个可用性条件去执行一块代码, 取决于你用的API是否运行时可用。一旦编译器验证API在代码块可以用,它就会用这些信息。
if #available(iOS 10, macOS 10.12, *) {
// Use iOS 10 APIs on iOS, and use macOS 10.12 APIs on macOS
} else {
// Fall back to earlier iOS and macOS APIs
}
上面的可用性条件指出, 对于iOS, if 语句只能在 iOS 10 和以后的版本上执行; 对于 macOS, 只能用在 macOS 10.12 和以后的版本。 最后一个参数, *, 是需要的,用来指定其他平台。if 语句执行在你指定的最小部署设备上。
一般形式中, 可用性条件带着一列平台名和版本号。平台名例如 iOS, macOS, watchOS, 和 tvOS—完整列表, 除了指定大版本号比如 iOS 8, 你还可以指定小版本号比如 iOS 8.3 和 macOS 10.10.3.
if #available(platform name version, ..., *) {
statements to execute if the APIs are available
} else {
fallback statements to execute if the APIs are unavailable
}
94.函数
函数包含执行特定任务的代码块。你给出一个函数名来表明它是做什么的, 需要时使用函数名来调用。
Swift 统一的函数语法是很灵活的, 可以像C语言一样没有参数名,也可以像objective-C一样带有名称和参数标签。参数可以提供默认值给简单的函数调用,同时可以作为输入输出参数传入。一旦函数调用就会修改传入的变量。
Swift 中的函数都有类型, 由参数类型和返回类型组成。你可以像使用其他类型一样使用这个类型, 这个让传递函数作为参数变得容易, 也可以从函数返回函数。 函数可以写在其他函数里, 在一个内嵌函数范围封装哟有用的功能。
定义和调用函数
当你定义一个函数时, 你可以定义一个或者多个命名, 函数的类型值作为输入, 也就是参数。你还可以定义一个值类型,函数执行完传回值。也就是返回值。
每个函数都有名字, 用来描述函数执行的任务。 为了使用一个函数, 你通过名字调用函数,传给它输入值 (参数) 匹配函数参数的类型。函数参数按照相同顺序提供。
下面例子里的函数叫 greet(person:), 因为它做的事情—它输入一个人的名字然后返回一个问候。为了完成这个, 你定义一个输入参数—一个字符串类型 person—然后返回一个字符串类型, 它包含了对这个人问候语。
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
所有信息都在函数里, 前缀是func 关键字。函数的返回类型用返回箭头-> (连字符跟着一个右方向箭头), 箭头后是返回的类型名。
定义描述了函数的功能, 希望接收的参数, 执行完成返回的值。 定义使得函数在代码各处可以清晰明确的调用:
print(greet(person: "Anna"))
// 打印 "Hello, Anna!"
print(greet(person: "Brian"))
// 打印 "Hello, Brian!"
在person 标签后出入一个字符串类型,调用 greet(person:) 函数, 比如 greet(person: "Anna"). 因为这个函数返回一个字符串类型, greet(person:) 可以被 print(_:separator:terminator:) 函数调用去打印返回值。 > **备注**
print(_:separator:terminator:) 函数第一个参数没有标签, 其他参数是可选的,因为他们有默认值。这些函数语法的变化在下面函数参数标签、参数名和默认参数值中描述。 greet(person:) 函数体先是定义了一个新的字符串常量 greeting,然后设置一个简单的问候信息。然后 greeting 作为返回值传出。函数结束执行然后返回greeting 的当前值。 你可以输入不同值多次调用 greet(person:) 函数。上面的例子展示了输入"Anna" 和 "Brian" 发生了什么。函数返回了定制的问候语。 为了让函数体变短, 你可以合并信息创建和返回语句到一行代码:
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// 打印 "Hello again, Anna!"
95.函数参数和返回值
Swift 中函数的参数和返回值非常灵活。 你可以定义任何,从带有不具名参数的简单工具函数到具有表达式参数名和不同参数选项的复杂函数。
没有参数的函数
函数不要求有输入参数。 这里有个没有输入参数的函数, 调用的时候总是返回相同字符串信息:
func sayHelloWorld() -> String {
return "hello, world"
}
print(sayHelloWorld())
// 打印 "hello, world"
函数定义是还是需要再函数名后加括号, 尽管它没有任何参数。函数调用的时候函数名后面还是跟着一堆括号。
多个参数的函数
函数可以有多个输入参数, 写在函数的括号里, 用逗号分开。
这个函数有两个参数,一个人名和是否他们已经被问候过。然后返回对这个人的问候语:
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} else {
return greet(person: person)
}
}
print(greet(person: "Tim", alreadyGreeted: true))
// 打印 "Hello again, Tim!"
你调用 greet(person:alreadyGreeted:) 函数,传入两个参数,一个是带有person标签的字符串值,一个是带有alreadyGreeted标签的布尔值。用逗号分开。注意这个函数跟上面的 greet(person:) 函数不同。 尽管两个函数名字一样, greet(person:alreadyGreeted:) 函数有两个参数而 greet(person:) 函数只有一个参数。
没有返回值的函数
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")
// 打印 "Hello, Dave!"
因为不需要返回一个值, 所以函数定义没有返回箭头 (->) 或者一个返回值。 > **备注**
严格来说, 这个版本的 greet(person:) 函数依然返回一个值, 尽管返回值没有定义。函数没有返回值是返回了一个特殊的类型 Void. 就是一个简单的空元组, 写作 (). 函数调用的时候返回值被忽略:
func printAndCount(string: String) -> Int {
print(string)
return string.characters.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// 打印 "hello, world" and returns a value of 12
printWithoutCounting(string: "hello, world")
// 打印 "hello, world" but does not return a value
第一个函数, printAndCount(string:), 打印一个字符串, 然后返回它的字符数。 第二个函数, printWithoutCounting(string:), 调用第一个函数, 不过忽略了它的返回值。一旦第二个函数被调用, 依然由第一个函数打印信息,但是不用返回值。
备注:返回值可以忽略, 但是如果函数说要返回值就一直要这么做。带有返回值的函数禁止控制流跳出函数底部,如果没有返回一个值的话。如果坚持这么做会导致编译错误。
带有多返回值的函数
你可以用元组做为函数的返回值,来实现返回多个值。
下面的例子定义了一个函数 minMax(array:), 用来茶盅整形数组里面的最小值和最大值:
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1.. currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
minMax(array:) 函数返回包含两个整数值的元组。这些值用最小和最大标签表明,这个可以在查询函数返回值的时候通过名字访问它们。
minMax(array:) 函数体开始时设置两个变量currentMin 和 currentMax值为数组第一项的值。然后函数开始遍历剩下的值,然后重复和 currentMin 和 currentMax 值进行大小比较。 最后, 最大值和最小值作为一个元组返回。
因为元组的成员值作为函数返回值被命名, 所以可以点语法来访问最大值和最小值:
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// 打印 "min is -6 and max is 109"
注意函数返回元组时,元组的成员不需要命名, 因为它们的名字作为函数返回类型已经被指定了。
96.可选元组返回类型
如果函数返回的元组类型有可能不存在值,你可以使用一个可选元组返回类型来反应整个元组可能为nil。一个可选元组返回类型括号后面跟着一个问号, 比如 (Int, Int)? 或者 (String, Int, Bool)?.
备注:可选元组类型比如s (Int, Int)? 不同于元组中包含可选类型比如 (Int?, Int?). 有一个可选元组类型, 整个元组都是可选的, 不单单是元组里的值。
minMax(array:) 函数返回的元组包含两个整数值。 然而, 函数不会对传入的数组做任何安全检查。 如果数组包含空数组, minMax(array:) 函数, 上面定义的, 尝试访问 array[0] 会触发运行时错误。
为了处理这种情况, minMax(array:) 函数返回值写成可选元组返回类型,如果数组为空则返回nil:
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1.. currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
你可以用一个可选绑定来判断, 这个版本的 minMax(array:) 函数是返回一个实际元组值还是nil:
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("min is \(bounds.min) and max is \(bounds.max)")
}
// 打印 "min is -6 and max is 109"
97.函数参数标签和参数名
每个函数参数既有参数标签也有参数名。参数标签在调用函数时使用; 函数中的每个参数调用时使用它们的标签。 参数名用于函数的实现。 默认情况, 参数使用参数名作为标签。
func someFunction(firstParameterName: Int, secondParameterName: Int) {
// In the function body, firstParameterName and secondParameterName
// refer to the argument values for the first and second parameters.
}
someFunction(firstParameterName: 1, secondParameterName: 2)
所有参数名必须唯一。 尽管多个参数可能有相同的标签, 唯一的参数标签会让你的代码更有可读性。
98.指定参数标签
参数标签写在参数名之前, 用空格分开:
func someFunction(argumentLabel parameterName: Int) {
// In the function body, parameterName refers to the argument value
// for that parameter.
}
这里有一个 greet(person:) 函数的变种,接受一个人名和家乡然后返回一个问候:
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// 打印 "Hello Bill! Glad you could visit from Cupertino."
参数标签的使用允许函数用表达方式调用, 像句子一样。
省略参数标签
如果你不想要参数标签, 写一个下划线代替显式的参数标签。
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// In the function body, firstParameterName and secondParameterName
// refer to the argument values for the first and second parameters.
}
someFunction(1, secondParameterName: 2)
如果一个参数有参数标签, 那么调用函数的时候参数必须带上标签。
默认参数值
你可以给函数参数赋一个默认值,写在类型的后面。如果定义了默认值, 调用函数时你就可以忽略这个参数。
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// If you omit the second argument when calling this function, then
// the value of parameterWithDefault is 12 inside the function body.
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault is 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault is 12
没有默认值的参数放在函数参数列表前, 带有默认值的放在后面。没有默认值的参数通常更重要—首先写它们更容易知道相同的函数被调用, 不用管默认参数是否被忽略。
可变参数
可变参数接受零个或者多个指定类型的值。你用可变参数指定函数调用时可以传入不同个数的输入值。可变参数写法是在参数类型名后写三个点。
传入可变参数的值在函数体中作为对应类型的数组是可用的。 例如, 带有数字名的可变参数和 Double… 在函数中作为常量数组 [Double]是可用的。
下面的例子计算任意长度的数字的平均数:
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers
备注:一个函数最多有一个可变参数。
输入输出参数
函数参数默认是常量。 尝试改变会导致编译期错误。这就意味着你不能错误的改变参数值。如果你想函数改变参数值, 想要这些改变持续到函数调用结束, 你可以定义输入输出参数来代替。
通过把 in-out 关键字写在参数类型前面来写一个输入输出参数。 输入输出参数有一个传入函数的值, 会被函数修改, 然后传出函数取代原有的值。 详情请参照 In-Out Parameters.
你可以只传入一个变量作为输入输出参数。 你不能传入一个常量或者字面值作为参数, 因为常量和字面量不能改变。当你使用它作为输入输出参数时,你可以直接在变量名前加上 (&), 来表示它可以被函数改变。
备注:输入输出参数不能有默认值, 可变参数不能标记为输入输出的。
这里有一个函数 swapTwoInts(::), 有两个输入输出整形参数 a 和 b:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoInts(::) 函数简单的交换a和b的值,这个函数通过把a的值存储到一个临时的常量temporaryA 来实现交换, 把b的值赋给a, 然后把 temporaryA 的值赋给b.
你可以调用 swapTwoInts(::) 函数交换两个整形变量的值。 注意 someInt 和 anotherInt 在传给函数的时候前面都加上了 &:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印 "someInt is now 107, and anotherInt is now 3"
上面例子展示 someInt 和 anotherInt 的原始值被 swapTwoInts(::) 函数改变, 尽管它们定义在函数之外。
备注:输入输出参数与函数返回值不一样。swapTwoInts 没有定义一个返回值或者返回一个值。但是它依然改变了 someInt 和 anotherInt 的值。 输入输出参数是函数影响外部的一种备选方式。
99.函数类型
每个函数都有一个指定的函数类型, 由参数类型和返回值类型组成。
例如:
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}
这个例子定义了两个简单的数学函数 addTwoInts 和 multiplyTwoInts. 两个函数都接受两个整形值, 然后返回一个整形值, 执行合适的数学运算会得出这个结果。
两个函数的类型都是(Int, Int) -> Int. 可以解读为:
“一个函数类型有两个参数, 两个都是整数类型t, 然后返回一个整形值”
这里有另外一个例子, 一个没有参数和返回值的函数:
func printHelloWorld() {
print("hello, world")
}
这个函数类型是 () -> Void, 或者 “一个函数没有参数,返回 Void.”
使用函数类型
使用函数类型跟Swift中其他类型很像。例如, 你可以定义一个函数的常量或者变量,然后把一个函数赋值给这个变量:
var mathFunction: (Int, Int) -> Int = addTwoInts
这段代码可以解读为:
“定义一个变量 mathFunction, 带有两个整形值的函数类型, 然后返回一个整形值。’ 调用函数 addTwoInts 给这个变量设置值。”
addTwoInts(::) 函数和 mathFunction 变量一样有相同的类型, 所有赋值是允许的。
你现在可以使用 mathFunction 名字调用赋值函数:
print("Result: \(mathFunction(2, 3))")
// 打印 "Result: 5"
有相同匹配类型的不同函数也可以赋值给相同的变量, 和非函数类型一样:
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// 打印 "Result: 6"
其他任何类型, 当你把函数赋值给一个常量或者变量时,你可以留给 Swift 去推断函数类型:
let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int
函数类型作为参数类型
你可以使用一个函数类型比如 (Int, Int) -> Int 作为另外一个函数的参数。当函数调用的时候,你可以把函数实现的一部分留给调用者。
这里有个一个例子打印上面数学函数的结果:
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// 打印 "Result: 8"
这个例子定义了一个函数 printMathResult(::_:), 有三个参数。第一个参数是 mathFunction, 类型 (Int, Int) -> Int. 你可以传递任何这种类型的函数作为第一个参数。 第二个和第三个参数是 a 和 b, 都是整型, 用来作为数学函数的输入值。
当 printMathResult(:::) 调用时, 传入 addTwoInts(:_:) 函数, 和整数 3 和 5. 调用提供的函数, 然后打印结果 8.
printMathResult(:::) 任务就是打印特定类型数学函数的结果。它不关心函数的实际实现—它只关心函数类型是否正确。 这使得 printMathResult(:::) 以一种类型安全的方式放手一些功能给函数的调用者。
函数类型和返回值
你可以用一个函数类型作为另外一个函数的返回类型。把一个完整的函数类型写在返回箭头后面就可以了。
下一个例子定义了两个简单的函数 stepForward(:) 和 stepBackward(:). stepForward(:) 函数返回一个比输入值大一的值, stepBackward(:) 函数返回一个比输入值小一的值。两个函数都有一个类型 (Int) -> Int:
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
这里有一个函数 chooseStepFunction(backward:), 返回类型是 (Int) -> Int. chooseStepFunction(backward:) 函数基于一个布尔参数backward 来返回 stepForward(:) 函数或 stepBackward(:) 函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}
现在你可以使用 chooseStepFunction(backward:) 去获取一个函数, 然后往前走或者往后走:
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero 调用 stepBackward() 函数
前面的例子决定是否需要一个负数或者正数步去移动currentValue ,让它逐渐趋近于0. currentValue 有个初始值3, 意味着 currentValue > 0 返回真, chooseStepFunction(backward:) 会返回 stepBackward(_:) 函数。 引用的返回函数存储在一个常量 moveNearerToZero.
moveNearerToZero 指向恰当的函数, 它可以用来计步到0:
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!
嵌套函数
到目前为止,你在本章看见的函数都是全局的示例, 它们定义在全局范围。你也可以在其他函数体内定义函数,这就是嵌套函数。
嵌套函数默认对外界隐藏, 但是对于它们的封闭函数依然可以使用。封闭函数可以返回一个嵌套函数, 并允许它给外部使用。
你可以重写上面的 chooseStepFunction(backward:) 例子来使用和返回嵌套函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!
100.闭包
闭包是自包含的功能块,可以传递和用在代码中。Swift 中的闭包和C 和 Objective-C 中的相似。
闭包可以捕获和存储上下文定义的任何常量和变量的引用。这就是关闭了常量和变量。Swift 替你处理闭包的所有内存管理。
备注:如果不熟悉闭包的概念不用担心。 下面会详细描述。
全局和嵌套函数实际上是一种特殊的闭包。闭包通常是下面三种形式之一:
全局函数是闭包,没有名字也不捕获任何值。
嵌套函数是闭包,有一个名字也可以从封闭函数中捕获值。
闭包表达式是匿名闭包,使用轻量级语法书写,可以捕获上下文的值。
Swift 的闭包表达式有一个清楚清晰的风格, 鼓励简洁,整齐的语法优化。这些优化包括:
从上下文推断参数和返回值的类型
从单个表达式闭包隐式返回
简约参数名
尾部闭包语法
闭包表达式
嵌套函数, 是一个方便的命名方法,也是在较大函数中定义自包含代码块的方式。不过, 有时候写一个更短的版本很有用, 它很像函数但是没有完整的声明和名字。函数作为参数的时候这个尤其有用。
闭包表达式是内联闭包的一种简写。 闭包表达式提供一些语法优化,以更短的样式书写闭包,但不失清晰和目的性。下面的闭包实例通过精炼一个排序函数来展示这些优化, 每个表达式功能一样,只是更简洁。
101.排序函数
Swift 标准库提供了一个方法 sorted(by:), 对已知类型数组值进行排序, 基于你提供的排序闭包输出。一旦排序完成, sorted(by:) 方法就会返回一个跟旧数组相同类型相同大小的新数组, 数组元素按照正确的顺序。原来的数组并没有被排序方法改变。
下面的闭包表达式使用 sorted(by:) 方法按照字母表倒序排列一个字符串数组。这是要排序的原来的数组:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:) 方法接受带有两个相同类型参数的闭包,作为数组的内容。然后返回一个布尔值,来说明排序后第一个值是出现在第二个值的前面还是后面。 这个例子是排序一个字符串值的数组, 所以排序闭包需要是一个函数类型 (String, String) -> Bool. 提供排序闭包的一个方式是写一个正确类型的函数, 然后把它作为参数传给 sorted(by:) 方法:
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一个字符串 (s1) 大于第二个字符串 (s2), backward(::) 函数返回真, 表明在排序后数组中 s1 应该出现在 s2 前面。 对于字符串中字符, “大于” 意思是 “字母表中出现较晚”。 也就说字母 “B” 大于字母 “A”, 字符串”Tom” 大于字符串 “Tim”. 这是提供了一个字母表的倒序, “Barry” 排在 “Alex”前面, 等等。
不过, 这种写法很冗长,它本质上是个单一表达式函数 (a > b). 这个例子里, 写成内联排序闭包更可取, 使用闭包表达式语法。
闭包表达式语法
闭包表达式一般是下面这种形式:
{ (parameters) -> return type in
statements
}
闭包表达式语法参数可以是输入输出参数, 但是它们不能有默认值。 如果你命名可变参数,那么可变参数也是可以用的。元组也可以用作参数和返回类型。 下面的例子展示了之前 backward(_:_:) 函数的闭包表达式版本:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
内联闭包的参数和返回类型声明跟backward(::)函数的声明一样。两种情况下, 它写作 (s1: String, s2: String) -> Bool. 不过, 对于内联闭包表达式, 参数和返回类型都写作花括号里,而不是外部。
闭包体以关键字in 开始。这个关键词的意思是闭包参数和返回类型的定义完成了, 闭包体开始了。
因为闭包体太短, 它甚至可以写成一行:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
这个说明 sorted(by:) 方法调用还是一样的。一对括号包括方法的所有参数。 只不过,现在参数在一个内联闭包里。
102.根据上下文推断类型
由于排序闭包作为参数传给函数, Swift 可以推断它的参数类型和返回值类型。sorted(by:) 方法被字符串数组调用, 所以它的参数类型是一个函数类型 (String, String) -> Bool. 这就是说 (String, String) 和 Bool 类型无需写出来作为闭包表达式定义的一部分。因为所有的类型都可以推断, 返回箭头 (->) 和参数名字外的括号都可以被忽略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
当把闭包传给一个函数或者方法作为内联闭包表达式时,总是可能要推断参数类型和返回类型。结果是, 一旦这个闭包用作函数或者方法参数,你就不需要用完整的形式书写内联闭包。
尽管如此, 如果你想你依然可以显式说明类型, 如果可以避免代码的二义性,这样做是鼓励的。sorted(by:) 方法这种情况, 闭包的目的很清晰,就是排序。读者假设这个闭包可能作用于字符串值是安全的。因为它帮助字符串数组的排序。
103.从单一表达式闭包隐式返回
通过忽略声明的返回关键字,单一表达式闭包可以隐式返回它们单一表达式的结果, 就像前一个例子的版本:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
这里, sorted(by:) 函数的方法参数清晰表明闭包要返回一个布尔值。因为闭包体包含了单一表达式(s1 > s2) ,而且返回值是布尔值, 这里没有二义性, 而且返回值可以忽略。
104.速记参数名
Swift 自动提供速记参数名给内联闭包, 可以用 $0, $1, $2, 来调用闭包参数值
如果你在闭包表达式使用这些速记参数名, 你可以忽略闭包定义的参数列表和个数,然后速记参数名类型会被期望的函数类型推断出来。 关键字in 也可以被忽略, 因为闭包表达式由它的整个包体组成。
reversedNames = names.sorted(by: { $0 > $1 } )
这里, $0 和 $1 调用闭包的第一个和第二个字符串参数。
105.运算符方法
这里其实有一个更短的方式去写上面的闭包。 Swift 的字符串类型定义了它特有的大于号的实现, 是带有两个字符串类型参数的方法,然后返回一个布尔类型。这很符合 the sorted(by:) 方法的类型需要。因此, 你可以简单的传入大于号, 然后 Swift 会推断你想要用它的字符串特有的实现:
reversedNames = names.sorted(by: >)
106.尾闭包
如果你需要传给函数一个闭包表达式作为最后的参数并且闭包表达式很长的话, 用尾闭包代替是很有用的。尾闭包写在函数调用的括号后面, 尽管它依然是函数的一个参数。 当你使用尾闭包语法时, 作为函数调用部分,你不要给闭包写参数标签。
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// 不使用尾闭包调用函数:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// 使用尾闭包调用函数:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
字符串排序闭包也可以使用尾闭包:
reversedNames = names.sorted() { $0 > $1 }
如果闭包表达式是作为函数或者方法的唯一参数,并且你使用这个表达式作为尾闭包的话, 你调用这个函数的时候可以不用在函数名后写括号:
reversedNames = names.sorted { $0 > $1 }
当闭包太长无法写成内联单行的时候, 使用尾闭包是最有用的。有一个例子, Swift 数组类型有个方法 map(_:) ,带有一个闭包表达式作为它的单一参数。数组中的每一项都会调用它一次, 为每项返回一个替代映射值 (可能是其他类型)。映射的性质和返回值的类型由闭包决定。
对每个数组元素应用提供的闭包后, map(_:) 方法返回包含新的映射值的新数组, 和原有数组对应值的顺序一致。
这里展示使用带尾闭包的 map(_:) 方法如何把整型值数组转换为字符串值数组。数组 [16, 58, 510] 用来创建新数组 [“OneSix”, “FiveEight”, “FiveOneZero”]:
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]
上面的代码创建了一个字典,在整数字和英文名之间建立迎神。同时定义了一个整数数组, 准备转换为字符串数组。
现在你可以用这个数字数组来创建一个字符串数组, 把闭包表达式作为尾闭包传入数组的 map(_:) 方法即可:
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 is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
数组每个元素调用一次 map(_:) 方法。 你不需要指定闭包输入参数的类型, number, 因为这个值可以被推断出来。
这个例子里, 变量 number 用闭包的 number 参数值来初始化, 所以这个值可以在闭包中修改。 (函数和闭包的参数总是常量的) 闭包表达式指定了一个字符串返回值类型, 来说明存储在映射输出数组中的类型。
每次调用闭包表达式创建一个字符串 output. 它使用余数运算符结算数字的最后一位 (number % 10), 然后用这个位去字典中查找对应的字符串。这个闭包可以用来创建任何大于0数字的字符串表示。
备注
调用字典下标后面跟着感叹号 (!), 因为字典下标返回一个可选值来表示如果键不存在查找就会失败。上面的例子里, 可以保证每次能找到有效的下标键, 所以感叹号用来强制拆包存在的值。
从字典获取的字符串会加到 output 前, 有效的建立了倒序的数字字符串版本。
然后数字变量除以10, 因为它是一个整数, 因为取整数倍, 所以 16 变成 1, 58 变成 5, 510 变成 51.
这个过程直到nubmer等于0, 这时闭包返回输出字符串, 然后通过 map(_:) 方法添加到数组。
上例尾闭包语法的使用,完整封装了闭包的功能。不要把整个闭包包含在 map(_:) 方法的method 外面括弧里。
107.捕获值
闭包可以在其定义的上下文中捕获常量和变量值。闭包可以在包体内调用和修改这些常量和变量值, 即使定义常量和变量的原来的范围不存在了。
在 Swift 中, 可以捕获值的最简单的闭包形式是一个内嵌函数, 写在另外一个函数体内。 内嵌函数可以捕获它外部函数的任何参数,而且可以捕获定义在外部函数的任何常量和变量。
这里有函数实例 makeIncrementer, 包含了一个内嵌函数 incrementer. 内嵌函数从它的上下文 incrementer() 捕获两个值, runningTotal 和 amount. 捕获这两个值以后, incrementer 被 makeIncrementer 作为闭包返回, 这个闭包每次调用时会把 runningTotal 增加 amount.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer 的返回类型是 () -> Int. 意思就是返回一个函数, 而不是一个简单值。返回的函数没有参数, 每次调用有一个整型值。
makeIncrementer(forIncrement:) 函数定义了一个整型变量 runningTotal, 用来保存incrementer 返回的增加的总步数。这个变量初始化值是 0.
makeIncrementer(forIncrement:) 有一个整型参数带有forIncrement 标签, 和一个变量名 amount. 这个参数传给指定 runningTotal 应该增加多少步数。makeIncrementer 定义了一个内嵌函数incrementer, 用来执行实际的递增操作。 这个函数简单把 amount 加到 runningTotal 上去然后返回结果。
单独拿出来, 内嵌函数 incrementer() 不太寻常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer() 函数没有任何参数, 然而它从函数体内部调用runningTotal 和 amount。 它从包围它的函数里捕获 runningTotal 和 amount. 引用捕获确保 runningTotal 和 amount 在调用 makeIncrementer 结束后不会消失, 也确保了 runningTotal 在下一次调用 incrementer 函数时依然有效。
备注:作为一个优化, 如果闭包修改一个值, 而且闭包创建后这个值没有改变的话,Swift 可能会不会捕获和保存这个值的副本。
Swift 同时处理了变量释放后的所有的内存管理。
这里有一个 makeIncrementer 活动的例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
这个例子设置了一个常量 incrementByTen,它调用增量器函数,这个函数每次调用把 runningTotal 变量加10. 多次调用效果如下:
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
如果你再创建一个增量器, 它会有自己新的单独的runningTotal 变量:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
调用原先的增量器(incrementByTen) 继续增加它的 runningTotal 变量, 不会影响 incrementBySeven 捕获的变量:
incrementByTen()
// returns a value of 40
备注:如果你把闭包赋值给一个类实例的属性, 然后闭包会通过引用这个实例或者它的成员来捕获这个实例。你将在闭包和实例之间建立一个强引用循环。 Swift 使用捕获列表来打破这种强引用循环。
闭包是引用类型
上面的例子, incrementBySeven 和 incrementByTen 都是常量, 但是这些常量引用的闭包依然可以改变它们已经捕获的 runningTotal 变量。 这是因为函数和闭包是引用类型。
每当你赋值函数或者闭包给一个常量或者变量, 实际上你是设置常量或者变量去引用函数或者闭包。上面的例子, incrementByTen 引用的闭包选择是常量, 而不是闭包本身的内容。
这也意味着如果你把一个闭包赋值给两个不同的常量或者变量,它们引用的是相同的闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
逃逸闭包
闭包被传给函数作为参数的事据说会逃逸, 但是会在函数返回时调用。 一旦你定义带有闭包作为参数的函数, 你可以在参数类型前书写 @escaping 来表示这个闭包允许逃逸。
闭包逃逸的方式之一是,通过存储在定义在函数外部的变量中。作为一个例子, 很多函数用闭包参数作为一个完成处理器来异步工作。 任务开始时函数返回, 但是函数完成之前闭包不会调用—这个闭包需要逃逸, 后面再调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,并且把它添加到外部定义的数组。如果你不把这个参数标记为 @escaping, 你会收到一个编译错误。
标记 @escaping 意思是你必须在闭包里显式引用self。例如, 下面的代码, 传给 someFunctionWithEscapingClosure(:) 的闭包是一个逃逸闭包, 意思是需要显式引用self。 相反, 传给 someFunctionWithNonescapingClosure(:) 的闭包是非逃逸的, 意思是可以隐式引用self.
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 参数只在调试模式执行, message 参数则在 condition 是假的时候执行。
自动闭包让你延迟执行, 直到你调用闭包内部代码才会运行。延迟作用对于边界影响和昂贵计算代码很有用, 因为它可以让控制代码什么时候执行。 下面的代码展示闭包的延迟执行。
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())!")
// 打印 "Now serving Chris!"
print(customersInLine.count)
// 打印 "4"
尽管 customersInLine 数组第一个元素被闭包内的代码给移除了, 但是直到这个闭包实际被调用,数组的元素才会被移除。如果闭包永不调用, 闭包内的表达式永远不会执行, 也就是说数组元素永不会被移除。注意 customerProvider 类型不是 String 而是 () -> String—不带参数的函数只是返回一个字符串。
当你把闭包传给函数时,你会得到相同的延迟执行效果。
// 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 .
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印 "Now serving Ewa!"
备注
过度使用自动闭包会让你的代码很难理解。 上下文和函数名字应该让人知道执行推迟了。
如果你想让自动闭包可以逃逸, 同时使用 @autoclosure 和 @escaping 属性。
// customersInLine is ["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!"
上面的代码, 不是调用作为 customerProvider 参数的闭包, collectCustomerProviders(_:) 函数添加闭包到 customerProviders 数组。 数组在函数外部声明, 意味着函数返回后数组中的闭包会执行。 结果就是, customerProvider 参数值必须被允许逃离函数范围。
108.枚举
一个枚举定义一组相关的常见类型的值, 让你在代码中用一个类型安全的方式工作。
如果你熟悉 C, 你就会知道 C 枚举会给相关的名字赋值一组整型值。 Swift 中的 枚举更加灵活, 不需要给每个枚举分支赋值。 如果提供一个值 ( “原始” 值), 这个值可以是字符串,字符,或者任意一个整型或者浮点型的值。
另外, 枚举的不同分支可以指定任何类型对应值。很像其他语言的 unions 或者 variants. 你可以定义一个常见的相关的分支集合来作为枚举的一部分。每部分都有相应的值对应它们。
Swift 的枚举是第一等类型。它们采用传统上只有类才支持的多种特性, 例如计算属性来提供关于枚举当前值的额外信息, 实例方法来提供有关枚举表示值的功能。枚举还可以用初始化来定义初始化值; 可以扩大它们原有实现的功能; 并且可以遵守协议去提供标准功能。
枚举语法
用enum关键字引入枚举,在大括号中定义整个枚举:
enum SomeEnumeration {
// enumeration definition goes here
}
这里有个指南者四个方位的例子:
enum CompassPoint {
case north
case south
case east
case west
}
定义在枚举中的值就是枚举分支 (比如 north, south, east, and west) 你可以用关键字来引入新的枚举分支.
备注
不像 C 和 Objective-C, Swift 枚举分支创建时不设一个默认的整数值。上面的 CompassPoint 例子, north, south, east 和 west 不会隐式等于 0, 1, 2 和 3. 相反, 不同的枚举分支在右方都有值t, 有一个显式定义的 CompassPoint 类型。
多个分支可以出现在一行,用逗号分开:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
每个枚举定义定义了一个全新的类型。 像Swift中的其他类型一样,它们的名字 (比如 CompassPoint 和 Planet) 应该以大写字母开始。 用单数名而不是复数名:
var directionToHead = CompassPoint.west
directionToHead 类型在初始化时推断类型。只要 directionToHead 声明为 CompassPoint, 你可以用一个短的点语法来给它设置一个不同的 CompassPoint 值:
directionToHead = .east
directionToHead 类型已经知道了, 所以设置值的时候可以丢掉类型了。使用显式类型枚举值时这个会让代码高度可读。
109.用Switch语句匹配枚举值
你可以用一个switch语句来匹配单个枚举值:
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 所有分支。 要求详尽保证枚举分支不会被疏忽掉。
如果提供所有的分支不合适, 你可以提供一个默认分支来覆盖没有显式指明的分支:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// 打印 "Mostly harmless"