Swift-- 闭包
闭包是自包含的功能块,可以在代码中传递和使用。Swift闭包与c和oc中的block类似,其他语言用lambdas。
闭包可以获取和存储指向在闭包内部定义的任何常量和变量,这就是所谓的封闭常量和变量,swift为你处理所有的捕捉的内存管理。
注意:如果你对捕捉的概念不熟悉,不用担心,在Capture Value中会详情的说明。
已经在Function中介绍的全局方法和嵌套方法,实际上是Closures的一个中特殊例子。Closures采用三种形式之一:
- 全局函数是一种 有个名字但是没有捕获任何值的闭包函数。
- 嵌套函数是一种 有一个函数名并且可以从他们的闭包函数中获取到值的闭包。
- 闭包表达式是使用轻量级语法编写的闭包,可以从周围环境捕获值。
Swift的闭包表达式有一种干净、清晰的风格,在常见的场景中使用了鼓励简短、无规则的语法的优化。这些优化包括:
从上下文推断参数和返回值类型
来自间单表达式闭包的隐式返回
速记参数名称
后关闭的语法
1.闭包的表达式
嵌套函数,是一种方便命名和定义自包函数块的方法,作为一个大函数的一部分。然而,有时候编写一个间断的不带完整的声明和命名的函数结构是很有用的,尤其是在需要用函数作为一个或者多个函数或者方法的参数的时候。
闭包表达式一种是简洁、集中的语法编写的内联闭包的方法。闭包表达式提供了一些语法优化,可以在短表单中编写闭包,而不会失去清晰或意图。下面闭包表达式的例子说明了这些优化,通过重新定义sorted(by:)函数,每个表达式都以更简洁的方式表达相同的功能。
(1)排序方法
Swift标准库提供一个sroted(by:)的方法,基于你提供的排序闭包输出,给一个已知类型的数组排序。一旦这个排序完成,sorted(by:)方法返回一个新的按照正确顺序排过序的数组,数组的类型和大小都与旧的数组一致。原始的数组不会被修改。
下面闭包表达式的例子用sorted(by:)方法为一个字符串数组按照字母顺序排序。排序(通过:)方法接受一个闭包,该闭包接受与数组内容相同类型的两个参数,并返回Bool值,以确定一旦值被排序,第一个值是否应该出现在第二个值之前或之后。如果第一个值出现在第二个值之前,那么排序结束需要返回true,否则将返回false。
这个例子是对字符串数组进行排序的,所这个排序的闭包需要函数类型为(String, String) -> Bool.
提供排序闭包的一个方法是写一个普通正确类型的函数,把这个函数对坐sorted(by:)方法的参数传递进去。
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"]
如果第一个字符(S1)比第二个字符串(S2)大,backward(_:_:)这个函数会返回true,说明在排序数组中S1应该拍在S2前面。
(2)闭包表达式的语法
{ (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 } )
(2)从上下文中推断类型
因为闭包函数作为一个参数传给一个方法,Swift可以推断这个参数的类型以及这个返回值的类型。sprted(by:)这个方法在字符串数组调用的时候,它的参数必须(String, String)->Bool 的函数类型。意味着(String, String)和 Bool 类型不需要作为闭包表达式定义的一部分。因为所有的类型都是推断,返回箭头(->)和参数名字的括号也可以省略。
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
当将闭包传给一个函数或者方法作为内联闭包表达时,通常都是可以推断出参数的类型和返回值类型的。
当然,如果你想的话,仍然让你的类型清晰一点,如果为了你的代码读起来避免模凌两可的时候还是鼓励大家那么坐的。
(3)从简单闭包表达式返回一个精确的值
简单表达式的闭包,在他们的表达式声明中省略return关键字,也可以精确返回一个结果。
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
这里,sorted(by:)方法的参数的函数类型让这个方法很清晰明白,这个闭包肯定返回一个Bool值。
(4)缩短参数名
SSwift自动为内联闭包提供了简短的参数名称,可以用来引用闭包参数的值,名称为$ 0、$ 1、$ 2,等等。
如果你在闭包表达式中用这些短参数的话,你可以在定义闭包的时候,省略参数列表。短参数的名称的个数和类型可以从指定的参数类型来推断出来。in关键字也可以被忽略,因为闭包表达式完全由它的闭包体组成。
reversedNames = names.sorted(by: { $0 > $1 } )
这里,$0,$1引用了闭包第一个和第二String类型的参数。
(5)操作方法
上面的闭包闭包表达式中还有一个更简短的方法。Swift的String类型定义了比操作符(>)做为一种具有两个字符串类型的参数和返回一个bool类型值的方法。因此,你可以简单地传一个比操作符,Swift会自动推断出你想用它的string-specific功能。
reversedNames = names.sorted(by: >)
想了解更多操作方法,请看“ Operator Methods”。
2.尾随闭包
如果你需要给一个函数传入一个闭包表达式作为函数最后的参数,而且这个闭包表达式很长的话, 你可以用尾随闭包来代替。尾随闭包写在函数调用的括号后面,但是它仍然是这个函数的参数。当你使用尾随闭包语法的时候,你不需要将闭包的参数标签写为函数调用的一部分。
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 }
在闭包表达式语法部分中的字符串排序闭包,可以写在sroted(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 是有闭包参数number初始化的值,所以这个值可以在闭包体中修改。(方法和闭包的参数始终都是常量)。闭包表达式也制定了一个返回类型String,为了表明在映射输出的数组中用这个类型排序。
这个闭包表达式被调用的时候会构建一个output的字符串。它通过使用剩余的运算符来计算最后一个数字(number % 10),然后用这个数字在digitNames的字典中,查找是否有匹配的字符串。
注意:对digitNames字典的下标的调用后面跟着一个感叹号(!),因为字典子脚本返回一个可选的值,以表明如果键不存在,字典查找就会失败。在上面的示例中,可以保证number % 10始终是数字字典的一个有效的下标键,因此使用感叹号来强制解压存储在下标可选返回值中的字符串值。
从digitNames字典中取得的字符串会添加在output前面,有效的构建了这个数字转换后的字符串。
变量number被10整除,因为它是一个整数,它在除法中是圆形的,所以,16变成了1,58变成了5,510变成了51.
这个过程一直重复知道这个number等于0,与此同时,output字符串也从闭包中返回,并添加到了map(_:) 方法的输出数组中。
在上述尾随闭包语法中,当函数的闭包一提供,就可以封装闭包功能,不需要在map(_:)方法的()内部封装整个闭包。
3.捕获值
一个闭包可以根据它定义的上下文来获取一个常量和变量。闭包可以在它的闭包体中引用和修改这些常量和变量的值,即使在最初定义的常量和变量的作用域不再存在。
在Swift中,可以捕获值的闭包最简单形式是嵌套函数,在闭包体中编写另外一个方法。一个嵌套函数可以获取到在它外面函数的参数,并且可以获取这些外函数定义的常量和变量。
这个例子就是一个叫makeIncrementer函数,它包含了一个嵌套函数incrementer。这个嵌套函数incrementer()函数可以从上下文中获取到两个值runningTotal和amount。获取到这两个值后,makeIncrementer将incrementer作为闭包返回,每次调用时递增。
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer }
makeIncrementer的返回类型是()->Int,这意味着它返回的是一个函数,而不是一个值。这个返回的函数没有参数,每次调用的时候返回Int值。
makeIncrementer(forIncrement:)函数定义一个runningTotal的整型变量,存储incrementer中正在运行总数的值,并且返回。这个整数初始化为0.
makeIncrementer(forIncrement:)函数有一个Int类型的参数,参数标签为forIncrement,参数名为amount。当调用incrementer这个方法时,传入参数指定runningTotal每次返回时增加了多少。在makeIncrementer函数中定义个了一个嵌套函数叫做incrementer,这个嵌套函数的作用是增加。这个嵌套函数只是简单地把amount与runningTotal的值相加,并返回相加的结果。
如果单独考虑这个嵌套函数,会觉得很奇怪:
func incrementer() -> Int { runningTotal += amount return runningTotal }
incrementer()函数没有任何的参数,它仍然在闭包体中引用runningTotal和amount。它从上下文函数中捕获了runnintTotal和amoun值,并在自己的函数体中使用了它们。通过引用获取值可以确保runningTotal和amount不会消失当makeIncrementer调用结束后,而且确保下一次incrementer函数调用的时候,runningTotal是有效的。
注意:
作为一种优化,如果不通过闭包来改变值,那么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
再一次调用incrementerByTen,会继续增加它自己的runningTotal变量,不会影响incrementBySeven的变量。
incrementByTen() // returns a value of 40
注意:如果你给一个类对象设置了闭包属性,而且这个闭包通过引用这个实例或者它的成员获取到这个实例,你会在闭包和实例中创建强的循环引用。
4.闭包是引用类型
上述的例子中,incrementBySeven和incrementByTen是常量,但是闭包常量的引用依然可以增加捕获到的runningTotal变量的值。这是因为函数和闭包都是引用类型。
无论什么时候,你分配一个函数或者闭包给一个常量或者变量,你实际上是把这个常量和变量指向了函数或者闭包。上面的例子,incrementByTen指的是一个常量,不是指闭包的内容。
它意味着如果你分配一个闭包给两个不同的变量或者常量,这两个常量或者变量指向相同的闭包:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // returns a value of 50
5.逃离闭包
如果一个闭包当成一个函数的参数传入时,这个闭包称之为逃离函数,但是它是在函数返回时才调用的。当你声明一个函数,函数的其中一个参数是闭包,你可以在参数类型前面写上“@escaping”表明闭包是允许逃离的。
还有一个闭包可以escape的方法是:通过存储在一个定义在函数之外的变量上。例如,许多启动异步操作的函数采用闭包参数作为完成处理程序。这个函数在操作开始时返回,但是闭包不会被调用知道这个操作完成--闭包需要转义,稍后调用。例如:
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) }
someFunctionWithEscapingClosure(_:)函数参数类型是闭包,并把它添加到在函数外的一个数组中。如果你不在函数的参数上标记“ @escaping
”,会提示编辑时错误。
通过@escape来控制闭包意味着您必须在闭包中显式地引用自显。例如,在下面的代码中,闭包传递给someFunctionWithEscapingClosure(_:)是一种escape closure,这意味着它需要显式地引用self。相比之下,闭包传递给someFunctionWithNonescapingClosure(_:)是一个nonescaping closure,这意味着它可以指自我暗示。
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"
6.自动闭包
自动闭包是一个闭包,它是自动创建的、用来作为函数参数的表达式。调用的时候,不需要传入任何的参数,返回这个表达式的值。这种句法方便可以让您在函数的参数周围省略大括号,而不是显式的closur,而是编写一个正常的表达式。
通过自动闭包调用函数是很正常的,但是实现这种方法并不常见。例如: assert(condition:message:file:line:)函数用自动闭包作为condition和message的参数,condition这个参数只有在编译debug时才调用,而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"
尽管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) } ) // Prints "Now serving Alex!"
上述的serve(customer:)函数采用这个精确的闭包,它返回一个用户的名字。下面那个版本的serve(customer:)执行相同的操作,但不是用精确的闭包,而是采用带有 @autoclosure的
自动闭包为参数作为参数的类型 。现在你可以调用这个函数用String作为闭包。
注意:过度使用自动闭包会让你代码看起来费力。
如果你想用一个自动闭包来允许逃逸,用 @autoclosure
and @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.") // Prints "Collected 2 closures." for customerProvider in customerProviders { print("Now serving \(customerProvider())!") } // Prints "Now serving Barry!" // Prints "Now serving Daniella!"