闭包
闭包是指能够在代码中使用和传递的自包含(self-contained)的块。
Swift中的闭包和C以及OC中的块很相似。它们可以捕获并且存储定义它们的上下文中的任何常量和变量的引用。Swift会处理捕获的内存管理,不需要开发者操作。
前面介绍的全局函数和嵌套函数,其实就是闭包的特例,闭包有以下三种形式:
1、全局函数是具有名字并且不捕获任何值的闭包
2、嵌套函数是具有名字并且能够从包含它的函数内部捕获值的闭包
3、闭包表达式是用轻量语法编写的没有名字切能从包含它的上下文中捕获值的闭包
Swift的闭包表达式风格十分简洁明确,并且做了很多优化,以鼓励在一般情况下使用简洁自由的语法,优化如下:
从上下文中侦测参数和返回值类型
单语句闭包隐式返回
参数名称简写
后置闭包语法
闭包表达式
嵌套函数就是一个作为更大函数的一部分的字包含的具名代码块的很好表现形式。但是,有时可能需要使用更简便的形式,比如没有名称和全部声明的构造。这在当你的操作那些接收别的函数当做参数的函数时尤为明显。
闭包表达式提供了一种简洁明确的语法来书写内联的闭包。闭包表达式提供了很多语法优化来使得简写形式的闭包不会变得意向模糊。
下边示例通过用越来越简洁的方式不断改进一个简单的sorted方法,来展现了闭包表达式的这种优化。
sorted方法是Swift的标准库提供的一个方法,它根据你提供的一个闭包来将已知类型的数组的元素进行排列。它接受两个参数,第一个是已知类型的数组,另一个是闭包,这个闭包接受两个同类型的参数,并且返回一个布尔值以确定第一个参数是否应该排在第二个参数前面。
假设一个数组是String类型的,那么这个sorted函数的第二个参数闭包就应该是(String,String) -> Bool函数类型。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
第一种提供排序闭包的方法是编写一个类型正确的函数,然后将函数作为第二个参数传递给sorted函数:
func backwards(s1: String, s2: String) -> Bool { return s1 > s2 } var reversed = sorted(names, backwards) // reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
当然,这种方式的写法比较繁琐,这个实例中,推荐使用闭包表达式语法编写更简洁的内联排序闭包。
闭包表达式的一般形式是:
{ (parameters) -> return type in statements }
闭包表达式语法可以使用变量参数、常量参数、inout参数。但是不能给参数提供默认值。可以在参数列表的最后给可变参数指定名称以使用可变参数。元组可以作为参数类型及返回值类型。
如下是上个例子中排序闭包的闭包表达式版本:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in return s1 > s2 })
注意,这里参数类型和返回值类型的写法与上个例子中的函数是有区别的,因为这里是写在大括号里边而非外边的。
这里的in关键字用以将两部分分开,标志这参数及返回值类型已经定义完毕,接下来是闭包体部分。这里in后边的换行并不是必须的,比如例子中闭包体很短,可以只写一行:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
从上下文中推断类型
因为排序闭包是作为一个参数传递给函数的,Swift能够推断出它的参数类型以及它从sorted函数的第二个参数的类型返回的值的类型。这个参数期望一个(String, String) -> Bool类型的函数,这意味着(String, String) 和 Bool 并不需要在闭包表达式的定义中写出来。因此更简洁的版本如下:
reversed = sorted(names, { s1, s2 in return s1 > s2 } )
当以闭包表达式的形式将一个闭包传递给一个函数的时候,其参数类型和返回值类型总是可以被推断出的。因此,当闭包被作为函数的参数时,都不需要写出它的完全形式。当然,如果在简写形式会造成语意不明的时候,推荐显示地写出完全形式。
单语句闭包的隐式返回
单语句闭包可以省略return关键字而隐式地返回结果,因此,可以进一步简化为:
reversed = sorted(names, { s1, s2 in s1 > s2 } )
参数名称简写
Swift自动给内联闭包提供参数名称的简写形式,通过$0,$1,$2...来指代第1,2,3...个参数。因此,排序闭包就可以简写为:
reversed = sorted(names, { $0 > $1 } )
这里,$0和$1指代了第一个和第二个String。
运算符函数
上面示例中的闭包表达式还可以更为简洁,Swift的String类型定义了“大于运算符(>)”的string版本的实现。这个实现是一个接受两个String类型参数并返回一个布尔类型的函数。这正好满足了sorted函数的第二个参数所需要的函数类型。因此,你可以只传递“大于运算符”进去,Swift会推断出你是想使用它的String版本的实现:
reversed = sorted(names, >)
后置闭包
如果你想将一个闭包表达式作为最后一个参数传递给某个函数,而且这个闭包表单时又很长的时候,就比较适合以闭包后置方式编写代码。后置闭包是指写在它支持的函数调用的圆括号后边的闭包表达式。比如:
func someFunctionThatTakesAClosure(closure: () -> ()) { // function body goes here } // here's how you call this function without using a trailing closure: someFunctionThatTakesAClosure({ // closure's body goes here }) // here's how you call this function with a trailing closure instead: someFunctionThatTakesAClosure() { // trailing closure's body goes here }
因此,排序闭包可以进一步简写为:
reversed = sorted(names) { $0 > $1 }
注意:如果闭包表达式是作为函数的唯一参数并且用后置的语法编写,那么可以省略函数名称后面的圆括号。
当闭包表达式特别长的时候,后置闭包就显得特别有用。比如Swift的Array有一个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] let strings = numbers.map { (var number) -> String in var output = "" while number > 0 { output = digitNames[number % 10]! + output number /= 10 } return output } // strings is inferred to be of type [String] // its value is ["OneSix", "FiveEight", "FiveOneZero"]
注意,这里的map后边省略了圆括号,因为闭包表达式是作为唯一参数被传入的;在闭包的参数列表中,使用了变量参数,这样就不用在闭包体内部在创建局部变量number然后把参数值赋给它。
这里还有一点需要注意的是 digitNames[number % 10]! 这里的感叹号,前面介绍过,字典的下标语法返回的是一个可选类型(Optional Type),这里根据上下文是可以确定这个值一定存在的,因此使用感叹号强制展开这个可选类型。
捕获值
闭包可以从它被定义的上下文中捕获变量或者常量。然后闭包就可以在自己的体内部引用或者改变这些常量或者变量,甚至在定义这些变量和常量的原始作用域已经不存在的情况下也是可以的。最简单的例子就是嵌套函数,函数内部定义的函数可以捕获外部函数内部的变量和常量,如果这个函数被返回了,它让然可以引用和操作这些常量和变量,尽管此时外部函数已经被返回不存在了。比如:
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer }
这里incrementer被外层函数返回之后,让然可以获取和操作runningTotal和amount,这里其实它捕获的是runningTotal的引用而不是值拷贝,因此在每次被调用的时候,都是更改的同一个变量,而amount并没有被改变,因此它是以值拷贝的形式被捕获的。
Swift会自动决定那些变量需要捕获引用,哪些需要捕获值拷贝,并且所有的内存管理都是Swift自动管理的,不需要开发者手动管理。
注意:如果你把一个闭包赋值给了一个类实例的属性,而这个闭包又通过引用那个实例或者它的成员而引用它本身,那么就在闭包和这个类实例之间形成了强引用循环。Swift通过引用列表来打破这种强引用循环,后面会有介绍。
闭包是引用类型
当把闭包赋值给某个常量的时候,仍然可以通过闭包改变它捕获的那些变量,这是因为函数和闭包是引用类型的,在赋值的时候,只是把它们的引用地址赋值给了变量,而不是其内部的代码块。就比如如果把一个闭包赋值给了两个常量,那么这两个常量其实指向了同一个闭包。