闭包能在其被定义的上下文中捕获和存储任何常量和变量的引用,这叫关闭这些常量和变量。
- 全局函数是有名字且不捕获任何值的闭包
- 嵌套函数是有名字且可从其函数捕获值的闭包
- 闭包表达式是用轻便的语法写的没名字的闭包,可从周围上下文捕获值
闭包有些特殊优化:
- 自动推断参数与返回值的类型
- 单行闭包隐性返回
- 速写参数名
- 尾部闭包语法
排序方法
Swift的标准库中提供一个sorted(by:),可以用来根据你提供的闭包的输出来对数组中元素进行排序。排序完成后该方法会返回一个新的排序后的数组,原数组没有被修改。
sorted(by:)方法接收一个闭包,这个闭包要传2个与数组元素类型相同的参数,并会返回一个Bool,为true时,第一个值会在第二个值的前面,为false时则反之。
例如下面,可以把普通的函数作为参数传过去
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
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"]
闭包表达式语法
通常如下:
{ (parameters) -> return type in
statements
}
所以上面的排序可以简写为:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
要注意的是这个闭包声明的参数与返回类型与 backward(::) 几乎一模一样,但,这是个包内表达式,参数与返回值是写在{}的里面。
闭包的包体被in关键字区分开来,in关键字代表了此闭包的定义(即参数与返回类型)结束了,包体要开始了。
从上下文中推断类型
因为排序闭包是作为一个参数传递给一个方法的,Swift可以推断出其参数的类型与返回类型。sorted(by:) 被一个含String元素的数组调用,所以其参数类型一定为(String, String) -> Bool,这意味着这些可以省去不写了。
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
单行闭包隐性返回
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
由于sorted(by:)方法的参数明确了需要一个Bool来作为闭包的返回值,并且闭包的包体包含的唯一一行表达式(s1 > s2)返回Bool,没有歧义,所以return可被省略。
速写参数名
Swift自动提供速写参数名(1, $2......)来指代闭包的参数,如果你在闭包中使用速写参数名,那么你可以省略定义参数,而参数的数量和类型会从函数类型中被推断出,此时in关键字也可以被省略。
reversedNames = names.sorted(by: { $0 > $1 } )
运算符方法
Swift的String类型定义了针对String类型的运算符>作为一个方法,此方法有两个String类型的参数,并返回Bool。刚好符合sorted(by:)需要的类型。因此,可以继续简写为:
reversedNames = names.sorted(by: > )
尾部闭包
如果你需要把闭包作为一个函数的最后一个参数传递,且此闭包表达式很长,可以将其写为尾部闭包。一个尾部闭包,虽然是一个参数,可以写在方法()的后面。当你使用尾部闭包语法时,你不需要写参数标签。
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
所以前面写的sorted(by:)也可以写为:
reversedNames = names.sorted() { $0 > $1 }
如果一个闭包是这个方法的唯一参数,那不需要写()
reversedNames = names.sorted() { $0 > $1 }
map(_:)
Swift的Array类型有map(:) 方法,接收一个闭包最为其唯一参数。数组中的每个元素都会使map(:) 会被调用一次,并返回映射过的值(可以是其他类型)。映射的特性与返回值的类型则由闭包来实现。
在闭包对每个元素执行过后,map(_:) 方法返回一个新的array,包含所有映射过的值,且对应的顺序与原数组相同。
下面是如何把[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]
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"]
用numbers数组通过传map(:)作为尾部闭包来创建strings数组。map(:) 方法会为数组中的每个元素调用闭包。你不需指定闭包输入的参数类型,因为类型可以从数组的值中被推断出。
在这个例子中,变量number用闭包的number参数来初始化,所以number可以被修改(函数和闭包的参数永远是常量)。闭包表达式也声明了一个返回类型String,来表明最终被存储到映射后数组的值类型。
值捕获
一个闭包可以从其定义的上下文中捕获常量或变量。然后闭包可在包体内引用或修改这些常量或变量,即使在原领域内这些常量或变量已不存在了。
下面是一个叫makeIncrementer的函数,其包含一个叫incrementer的嵌套函数。incrementer()函数捕获2个值,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。这意味着它返回一个函数,而不是简单的值。它还定义了一个变量runningTotal,来储存当前递增的总次数。
如果将上面嵌套的incrementer()函数抽出来看,似乎有点点奇怪
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
这个函数没有任何参数,然而却引用了runningTotal和amount。这是通过捕获他俩的引用来实现的。捕获引用确保了再makeIncrementer结束时,runningTotal和amount不会消失,并且在下一次incrementer被调用时runningTotal可用。
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
如果你创建另外一个incrementer,他会有其自己新的,分开的runningTotal:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40
Note:
如果把闭包赋值给一个类实例的属性,那么这个闭包会捕获此实例或其成员,造成循环引用。Swift用capture lists来打破循环引用。
闭包是引用类型
在上面的例子中,incrementBySeven和incrementByTen为常量,但是这些常量引用的闭包仍然能够递增其捕获的runningTotal变量。这是因为函数与闭包属于引用类型。
每当你赋值一个函数或闭包给一个常量或变量,你是将常量或变量设置为函数或闭包的一个引用。上面例子中,incrementByTen常量引用的是要选择哪个闭包,而不是闭包的内容。
这也意味着当你把一个闭包赋值给两个不同的常量或变量,他们均将引用这个相同的闭包。
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60
闭包逃逸
当闭包被用作参数传递,并且在函数调用结束后被调用时, 被称作是逃逸。当你声明一个参数含有闭包的函数时,你可以在参数类型前写@escaping来表示此闭包允许逃逸。
闭包逃逸的一种方式是将其存储在函数外的一个变量中。函数在其开始运行后返回,但闭包会直到运行结束后被调用。
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:) 函数接收一个闭包作为参数并将其添加到一个在函数外声明的数组。如果你没有用@escape标记参数,会得到一个编译期错误。
用@escape标记闭包意味着你需要在比包内显性引用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)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"