Swift闭包基础要点
闭包是自包含的函数代码块,可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。Swift会自动管理在捕获过程中涉及的内存操作。
闭包的形式:
- 全局函数:一个有名字但不会捕获任何值的闭包
- 嵌套函数:一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式:一个利用轻量级语法所写的可以捕获上下文中变量或常量值的匿名闭包
闭包表达式
闭包表达式一般形式:
1 | { |
示例:
Swift标准库提供了sorted(by:)
方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sorted(by:)
方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。下面的闭包表达式使用sorted(by:)
方法对一个Sring类型的数组进行字母逆序排序:
1 | let names = ["Baylee","Tom","Barry","Diniella"] |
说明:如果第一个字符串(s1)大于第二个字符串(s2),backward(::) 函数会返回 true,表示在新的数组中 s1 应该出现在 s2 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 “B” 大于字母 “A” ,字符串 “Tom” 大于字符串 “Tim”。该闭包将进行字母逆序排序,”Barry” 将会排在 “Alex” 之前。
根据上下文推断类型
因为排序闭包函数是作为sorted(by:)
方法的参数传入的,Swift可以推断其参数和返回值的型。sorted(by:)
方法被一个字符串数组调用,因此其参数必须是(String, String) -> Bool
类型的函数。这意味着(String, String)
和Bool
类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断:
1 | reverseNames = names.sorted(by: { s1,s2 in return s1 > s2 } ) |
单表达式闭包隐式返回
单行表达式闭包可以通过省略return
关键字来隐式返回单行表达式的结果:
1 | reverseNames = names.sorted(by:{ s1, s2 in s1 > s2}) |
参数名称缩写
Swift自动为内联闭包提供了参数名称缩写功能,可以直接通过$0,$1,$2来顺序调用闭包的参数。
如果你在闭包表达式中使用参数名称缩写,则可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时表达式完全由闭包函数体构成:
1 | //$0,$1表示闭包中第一个和第二个String类型的参数 |
运算符方法
Swift的String类型定义了关于>
的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。而这正好与sorted(by:)
方法的参数需要的函数类型相符,所以可以简单地传递一个大于号(>
),Swift可以自动推断出你想使用大于号的字符串函数实现:
1 | reverseNames = names.sorted(by: >) |
尾随闭包
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
1 | func someFunctionThatTakesAClosure(closure: () -> Void) { |
在闭包表达式语法一节中作为 sorted(by:) 方法参数的字符串排序闭包可以改写为:
1 | reversedNames = names.sorted() { $0 > $1 } |
如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:
1 | reversedNames = names.sorted { $0 > $1 } |
Swift的Array
类型有一个map(_:)
方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。
当提供给数组的闭包应用于每个数组元素后,map(_:)
方法将会返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。
1 | let digitNames = [0:"zero",1:"one",2:"two",3:"three",4:"four",5:"five",6:"six",7:"seven",8:"eight",9:"nine"] |
值捕获
闭包可以在其被定义的上下文中捕获常量和变量。可以捕获值的闭包的最简单的形式就是嵌套函数。嵌套函数可以捕获其他外部函数所有的参数以及定义的常量和变量。
1 | func makeIncrementer(forIncrement amount: Int) -> () -> Int { |
捕获引用保证了 runningTotal 和 amount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在。
调用makeIncrementer
:
1 | let incrementByTen = makeIncrementer(forIncrement: 10) |
如果你创建了另一个 incrementer,它会有属于自己的引用,指向一个全新、独立的 runningTotal 变量:
1 | let incrementBySeven = makeIncrementer(forIncrement: 7) |
再次调用原来的 incrementByTen 会继续增加它自己的 runningTotal 变量,该变量和 incrementBySeven 中捕获的变量没有任何联系:
1 | incrementByTen() |
如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用
闭包是引用类型
无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
1 | let alsoIncrementByTen = incrementByTen |
逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用:
1 | var completionHandlers: [() -> Void] = [] |
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为@escaping
,就会得到一个编译错误。将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。
1 | func someFunctionWithNonescapingClosure(closure: () -> Void) { |
自动闭包?????
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用时,会返回被包装在其中的表达式的值。可以省略闭包的花括号,而用一个普通的表达式来代替显式的闭包。
自动闭包能够延迟求值,因为直到调用这个闭包,代码段才会被执行。
1 | var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |