闭包是独立的功能块,在你的代码中可以传递和使用。swift中的闭包与C和Objective-C中的块和其他编程语言中的匿名函数类似。
闭包可以捕捉和存储它定义的上下文环境中的任何常量和变量的引用。这就是封闭那些常量和变量。swift为你处理全部捕捉的内存管理。
如果你对捕捉的概念不熟悉不用担心。在下面的Capturing Values中详细的解释了。
全局和内嵌的函数,在Functions中介绍的,实际上是闭包的特殊情况。闭包有三种形式:
- 全局函数是有名字并且没有捕捉任何值的闭包
- 内嵌函数是有名字并且捕捉了他们封闭函数的值的闭包
- 闭包表达式是可以从他们周围上下文捕捉值的用轻量语法写的无名的闭包。
swift的闭包表达有一个干净,清晰地风格,具备在普通场景下鼓励简洁,自由混杂的语法的最佳优化。这些优化包括:
- 从上下文推导参数并且返回值的类型
- 从一个单一表达式闭包中默认返回
- 速写参数名
- 闭包尾语法
闭包表示(Closure Expressions)
内嵌函数,像Nested Functions中介绍的,是将命名和定义独立代码块作为长函数一部分的简便方式。不过,有时候不用全声明和名称来写像函数结构的简短版本是有帮助的。当你使用将函数作为他们一个或多个参数的函数或者方法时特别有用。
闭包表达是将内联闭包用简短,集中的语法表达的一种方式。闭包表达为没有缺失清晰或者目的特性的简短的形式写闭包提供了许多最优化的语法。下面的的闭包例子通过多次反复重定义一个sorted(by:)简单的例子解释了这些优化,每一个用一个更简短的方式表达了相同的功能。
排序方法(The Sorted Method)
swift的标准库提供了一个名为sorted(by:)的方法,对一个已知类型的值的数组进行排序,以一个你提供的排序闭包的输出为基础。一旦你完成了排序过程,sorted(by:)方法返回一个和老的一样类型和大小的数组,它的元素是正确的分类顺序。原来的数组没有被sorted(by:)方法修改。
下面的闭包表达例子使用sorted(by:)方法来String值的数组进行倒字母顺序排列。这里是要排序的数组的初始化:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]sorted(by:)方法接受一个接受两个相同类型作为数组内容的闭包,并返回一个Bool值来说明当值进行排序时第一个值是否应该出现在第二个值之前或者之后。如果第一个值应该出现在第二个值之前那么排序闭包应该返回一个true,否者返回false。
这个例子是对一个String值的数组进行排序,所以排序闭包需要是一个(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(_:_:)会返回true,证明在排序的数组中s1应该出现在s2之前。对于字符串中的字母,“greater than”意味着“比在字母表中出现的晚“。这代表字母”B“比字母”A“大,”Tom“比”Tim“大。这是一个逆字母表排序,”Barry“放在”Alex“前面,等等。
不过,这是一个冗长的方式来写实质上是一个单一表达式的函数(a>b)。这个例子中,更喜欢用内联排序闭包,使用闭包表达语法。
闭包表达语法(Closure Expression Syntax)
闭包表达语法有下面常见的形式:
{ (parameters) -> return type in
statements
}闭包表达语法中的参数可以使in-out参数,但是他们不能有默认值。如果你命名了一个可变参数可以是用可变参数。元祖也可以用作参数类型和返回类型。
下面的例子展示了上面函数backward(_:_:)的闭包表达版本:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})这一这个内联闭包的参数和返回类型的声明和backward(_:_:)函数的是一样的。两种情况中,写作(s1:String,s2:String)->Bool。不过,对内联闭包表达来说,参数和返回类型写在大括号之内,不是之外。
闭包体的开始通过in关键字标记。这个关键字指示闭包参数he返回类型的定义结束了,必报的主体将要开始。
因为闭包的主体很短,他可以写在一行里:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )这个全部是对sorted(by:)调用的例子保留的一样。一对括号将整个方法的参数打包。不过,参数现在是一个内联闭包。
从上下文推导类型(Inferring Type From Context)
因为排序闭包作为一个参数传给方法,swift可以推导它的参数的类型和它返回值的类型。方法sorted(by:)被在一个字符串数组上调用,所以它的参数一定是一个(String,String)->Bool类型的函数。这意味着(String,String)和Bool类型不需要作为闭包表达定义的一部分。因为全部的类型可以推导,参数名称边上的括号和返回箭头(->)都可以忽略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )当作为一个内联表达传一个闭包给函数或者方法时都可以推导参数类型和返回类型。所以,当闭包用作函数或者方法参数时不需要将内联闭包写成完整的形式。
但是,如果你希望的话仍然可以使类型是明确的,如果避免读者对你的代码的疑惑,鼓励这样做。在sorted(by:)方法的情况中,从发生排序的事实中看到闭包的目的非常清晰,对读者来说很安全来假设闭包似乎对String的值进行操作,因为它是协助一个strings数组的排序。
从单表达时的闭包中潜在返回(Implicit Returns from Single-Expression Closures)
单表达式的闭包通过从声明中忽略return关键字来隐式的返回他们单表达式的结果,像之前例子的这个版本:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )这里,方法sorted(by:)参数的函数类型使闭包必须要返回一个Bool值非常清晰。因为闭包体包含单一的一个返回Bool值的表达式(s1>s2),没有歧义,return关键字可以忽略。
简写参数名称(ShortHand Argument Names)
swift自动给内联闭包提供简写参数,可以用来通过名称$0,$1,$2等等来引用闭包参数的值。
如果你在闭包表达式中使用这些简写的参数名,可以忽略闭包定义中的参数列表,简写参数名的数字和类型从预料的函数类型中推导。in关键字也可以忽略,因为闭包表达是完全构成了他的主体:
reversedNames = names.sorted(by: { $0 > $1 } )这里,$0和$1引用必报的第一个和第二个String参数。
运算方法(Operator Methods)
实际上有一个更短的方式来写上面的闭包表达式。swift的String类型将它的大于号(>)的string-specific实现定义为一个有两个String类型参数和Bool返回值的方法。这正匹配了方法sorted(by:)需要的方法类型。所以你可以只传一个大于号,swift将会推导出你想使用string-specific实现:
reversedNames = names.sorted(by: >)更多关于操作方法的信息,查看Operator Methods。
尾部闭包(Trailing Closures)
如果你想传给函数一个闭包表达式作为函数的最后一个参数并且闭包表达很长,用尾部闭包写会有帮助。尾部闭包写在函数调用括号的后面,即使它对函数来说仍然是一个参数。当你使用尾部闭包语法时,不用作为函数调用的一部分给闭包写形参标签。
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
}上面Closure Expression Syntax章节中字符串排序闭包可以作为尾部闭包写在方法sorted(by:)方法之外:
reversedNames = names.sorted() { $0 > $1 }如果一个闭包表达用来作为函数或者方法的唯一参数并且你用尾部闭包提供了表达式,当你调用函数的时候不需要在函数或者方法后面写一对括号:
reversedNames = names.sorted { $0 > $1 }当闭包特别长,不能在一行上写内联时,尾部闭包非常有用。和例子中一样,swift的数组类型有map(_:)方法可以将闭包表达作为它单一的参数。数组中的每个对象调用一次闭包,并未那个对象返回一个另一个映射的值(可能是其他类型)。映射的本质和返回值的类型留给闭包去指定。
在给每个数组的元素使用提供的闭包之后该,方法map(_:)返回一个包含全部新的映射值的数组,和他们在原来数组中对应的值一样的顺序。
这里是你如何用一个尾部闭包使用方法map(_:)来将一个Int值的数组转变成String值的数组。数组[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]上面的代码创建了一个在整型数字和他们名字英文版本之间映射的字典。也定义了一个整型数组,准备转换成字符数组。
你现在可以使用numbers数组来创建一个String值的数组,通过作为尾部闭包将闭包表达式传给数组的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参数初始化,所以它的值可以在闭包体中修改。(对函数和闭包的参数通常是常量)。闭包表达也制定一个String类型返回,指定将要存储在映射的输出数组的值的类型。
闭包表达式在他每次调用时创建一个名为output的字符串。通过使用取余运算符(number%10)计算number最后的数字,用这个数字在digitNames字典中找一个合适的字符串。闭包可以用来创建一个表示任何大于0的字符串。
digitName字典的下标的调用跟着感叹号(!),因为字典下标返回一个可选类型指示如果key不存在字典查找可能失败。在上面的例子中,保证对于字典digitNames,number%10会一直是有效的下标key,所以用感叹号强解包存在下标的可选返回值的String值。
从digitNames字典中查找到的string加到输出的前面,实际上创建了反向的一个字符串版的number。(表达式number%10给了16一个值6,58时是8,510时是0)。
变量number被10除。因为它是一个整型,在除法时向下取整,所以16变成1,58变成5,510变成51.
直到number等于0该过程一直重复,这个时候闭包返回输出值,通过map(_:)方法添加到输出数组中。
上面例子中尾部闭包语法的使用紧跟在闭包支持的函数后面将闭包的功能整齐的封装起来,不需要将整个闭包包在方法map(_:)外面的括号中。
捕捉值(Capturing Values)
闭包可以从定义它的周围环境中捕捉常量或者变量。然后闭包可以在它的主体中引用和修改这些常量和变量的值,即使定义常量和变量的原来的区域已经不存在了。
在swift中,可以捕捉值的闭包最简单的形式是内嵌函数,写在其他函数体中。内嵌函数可以捕捉它外部函数的任何参数并且也能捕捉在外部函数中定义的任何常量和变量。
这里是一个名为makeIncrementer的函数的例子。它包含一个名为incrementer的内嵌函数。内嵌invrementer()函数捕捉两个值,runingTotal和amount,从他周围的上下文中。捕捉完这些值后,incrementer作为一个闭包被makeIncrementer返回,每次他被调用的时候runintTotal加amount。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}makeIncrementer的返回类型是()->Int。这意味着它返回一个函数而不是一个简单的值。它返回的函数没有参数,并且它每次调用的时候返回一个Int值。学习函数如何返回其他函数,查看Function Types as Return Types。
函数makeIncrementer(forIncrement:)定义了一个名为runingTotal的整型变量,来存储incrementer当前运行的总数。变量用0初始化。
函数makeIncrementer(forIncrement:)有一个有forIncrement作为形参标签的Int参数,和一个名为amount的参数。传给这个参数的参数值制定了每次返回的incrementer函数被调用runningTotal应该增加多少。函数makeIncrementer定义了一个名为invrementer的函数,它执行真正的加法。这个函数简单的给runningTotal加amount并将结果返回。
当单独考虑的时候,内嵌函数incrementer()看起来可能不常见:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}函数incrementer()没有任何参数,但是从他的函数体重引用runningTotal和amount。它从周围函数中捕捉指向runningTotal和amount的引用并且在他自己的函数体中使用。通过引用捕捉确保了当调用完makeIncrementer时runningTotal和amount不会消失,也确保了下一次incrementer函数调用的时候runningTotal可以获取。
作为优化,如果那个值不会被闭包修改并且如果在闭包创建后不会修改,swift可能替换捕捉并存储值的一个复制体。当它们不再需要的时候swift也会处理包含在处理变量中的内存管理。
下面是makeIncrementer的一个实例:
let incrementByTen = makeIncrementer(forIncrement: 10)这个例子设置了一个名为incermentByTen的常量来引用函数incrementer,它每次调用都给他的变量runingTotal加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调用原来的incrementer(incrementByTen)会继续增加他自己的runningTotal变量,不会影响incrementBuSeven捕捉的变量:
incrementByTen()
// returns a value of 40如果你给一个类的实例属性分配一个闭包,闭包通过引用实例或者他的属性不活了那个实例,你会在闭包和实例之间形成一个强引用循环。swift使用捕捉列表来打破这种强引用循环。更多信息,查看Strong Reference Cycles for Closures。
闭包是引用类型(CLosures Are Reference Types)
在上面的例子中,incrementBySeven和incrementByTen是常量,但是那些常量引用的闭包仍可以增加他们捕捉的runningTotal变量。这是因为函数和闭包是引用类型。
无论什么时候你给常量或者变量分配一个函数或者闭包,实际上你是把常量或者变量设置为一个纸箱函数或者闭包的引用。在上面的例子中,incrementByTen引用的闭包的选择是常量,不是闭包自身的内容。
这也意味着,如果你将一个闭包分给两个不同的常量或者变量,这两个常量或者变量医用相同的闭包。
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60上面的例子展示了调用alsoIncrementByTen和incrementByTen是一样的。因为他们两个引用了相同的值,他们两个都是增加并返回相同的。
逃逸闭包
当闭包作为参数传给函数,但是在函数返回值后调用时闭包称为逃逸一个函数。当你声明一个将闭包作为他的参数的函数时,你可以在参数类型前些@escaping指明闭包允许逃逸。
闭包逃逸的一种方式是存在一个定义在函数之外的变量中。例如,很多开始异步操作的函数把闭包参数作为完成的操作。在他开始操作之后函数返回,但是直到操作完成闭包才调用--闭包需要逃逸,过后再调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}函数someFunctionWithEscapingClosure(_:)采用闭包作为它的一个参数并且将它添加到定义在函数之外的数组中。如果你咩有用@escaping标记这个函数的参数,将会有编译错误。
使用@escaping标记闭包意思是你不得不在闭包中明确的引用self。例如,下面的代码中,传给someFuncitionWithEscapingClosure(_:)的闭包是一个逃逸闭包,意味着他需要明确的引用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"自动闭包(Autoclosures)
一个自动闭包是一个自动创建来打包作为参数传给函数的表达式的闭包。不需要任何参数,然后后当它调用的时候,返回打包在他中的表达式的值。这个语法的便利可以让你通过写一个普通的表达式代替明确的闭包来忽略函数参数周围的大括号。
调用带有自动闭包的函数没有特别的,但是实现这种的函数不普通。例如,函数assert(condition:message:file:line:)对它的condition和message参数使用自动闭包;它的condition参数只在调试构建的时候调用而它的message参数只有condition是false时才执行。
自动闭包让你延迟执行,因为其中的代码知道你调用闭包才执行。延迟执行对有副作用或者性能消耗特别大的代码很有用,因为它使你可以控制代码什么时候执行。下面的代码展示了一个闭包如何延迟执行。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"即使数组customersIntLine的第一个元素被闭包中的代码移除了,直到闭包被真正调用数组的元素没有被移除。如果闭包永远不被调用,闭包中的表达式永远不会执行,意味着数组的元素不会被移除。注意customerProvider的类型不是String而是()->String--一个没有参数返回一个string的函数。
当作为参数传递给函数一个闭包时得到和演示执行一样的效果。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"上面列表中函数serve(customer:)使用一个返回客人名字的明确的闭包。下面版本的serve(customer:)展示了一样的操作,但是替代了使用明确的闭包,他通过使用@autoclosure属性标记参数的类型来使用一个自动闭包。现在你可以像他代替闭包使用了一个String参数调用函数。参数自动转化为一个闭包,因为customerProvide参数的类型由@autoclosure属性标记了。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"过度使用自动闭包会让你的代码阅读困难。上下文和函数名应该清晰地看出延时执行。
如果你想要一个可以逃逸的自动闭包,使用@autoclosure和 @escaping两个属性。@escaping属性的表述在上面Escaping Closures。
// 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.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"上面的代码中,取代了调用作为他的参数customerProvide传递的闭包,函数collectCustomerProviders(_:)增加了闭包在数组customerProviders中。数组在函数区域之外声明,意味着数组中的闭包可以在函数返回之后执行。结果,参数customerProvider的值必需允许逃逸函数的区域。