前言
-
闭包是一个自包含的功能性代码模块。
- 一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号 “{}” 来表示闭合并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包。
- 通俗的解释就是一个
Int
类型里存储着一个整数,一个String
类型包含着一串字符,同样,闭包是一个包含着函数的类型。 - Swift 中的闭包与 C 和 OC 中的
Block
以及其他一些编程语言中的lambdas
比较相似。Block
和闭包的区别只是语法的不同而已,而且闭包的可读性比较强。
-
在 Swift 中闭包有着非常广泛的应用,有了闭包,你就可以处理很多在一些古老的语言中不能处理的事情,这是因为闭包使用的多样性。
- 比如你可以将闭包赋值给一个变量。
- 你也可以将闭包作为一个函数的参数。
- 你甚至可以将闭包作为一个函数的返回值。
- 闭包可以捕获和存储其所在上下文中任意常量和变量的引用,并且 Swift 会为你管理在捕获过程中涉及的所有内存操作。
-
闭包是引用类型的。
- 无论你将函数/闭包赋值给一个常量还是变量,实际上都是在将常量/变量设置为对应函数/闭包的引用,这也意味着如果你将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包。
- 在使用闭包时需要注意循环引用。
-
从本质上来说,函数、方法和闭包是一体的,闭包的功能类似于函数嵌套,但是闭包更加灵活,形式更加简单。在 Swift 语言中有三种闭包形式。
- 全局函数:是一个有名字但不会捕获任何值的闭包。
- 嵌套函数:是一个有名字并可以捕获到其封闭函数域内的值的闭包。
- 匿名闭包:闭包表达式是一个利用轻量级语法所写的,可以捕获其上下文中变量或常量值。
1、闭包的形式
1.1 函数形式闭包
-
函数形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] func myConpare(s1: String, s2: String) -> Bool { return s1 > s2 } let names = namesArray.sort(myConpare)
1.2 一般形式闭包
-
一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号 “{}” 来表示闭合并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包。
{ (参数名1: 参数类型, 参数名2: 参数类型, ...) -> 返回值类型 in 语句组 }
- 闭包写在一对大括号
{}
中,用in
关键字分割。 in
后的语句是闭包的主体。in
之前的参数和返回值类型是 “语句组” 中所使用的参数和返回值格式的一种指示,并不必在语句组中进行逻辑运算与返回。- 可以使用常量、变量、
inout
、可变参数、元组类型作为闭包的参数,但不能在闭包参数中设置默认值,定义返回值和函数返回值的类型相同。 - 闭包表达式的运算结果是一种函数类型,可以作为表达式、函数参数和函数返回值。
- 闭包写在一对大括号
-
一般形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort { (s1: String, s2: String) -> Bool in return s1 > s2 }
1.3 参数类型隐藏形式闭包
-
Swift 中有类型推断的特性,可以根据上下文推断出参数类型,所以我们可以去掉参数类型。
{ (参数名1, 参数名2, ...) -> 返回值类型 in 语句组 }
-
参数类型隐藏形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort { (s1, s2) -> Bool in return s1 > s2 }
1.4 返回值类型隐藏形式闭包
-
Swift 中有类型推断的特性,可以根据上下文推断出返回值类型,所以我们可以去掉返回值类型。
{ (参数名1, 参数名2, ...) in 语句组 }
-
返回值类型隐藏形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort { (s1, s2) in return s1 > s2 }
1.5 return 隐藏形式闭包
-
如果在闭包中只有一条语句,比如示例中的
return s1 > s2
,那么这种语句只能是返回语句,此时关键字return
可以省略,省略后的格式变为一种隐式返回。{ (参数名1, 参数名2, ...) in 语句组(省略 return) }
-
return
隐藏形式闭包let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort { (s1, s2) in s1 > s2 }
1.6 参数名省略形式闭包
-
闭包的使用非常的灵活,我们可以省略闭包参数列表中的参数的参数类型定义,被省略的参数类型会通过闭包函数的类型进行推断。
-
同时,我们也可以在闭包函数体中通过使用闭包的参数名简写功能,直接使用
$0
、$1
、$2
等名字就可以引用闭包的参数值,$0
指第一个参数,$1
指第二个参数, Swift 能够根据闭包中使用的参数个数推断出参数列表的定义。 -
如果同时省略了参数名和参数类型,那么 in 关键字也必须被省略,此时闭包表达式完全由闭包函数体构成。
{ 语句组(使用 $0、$1、$2) }
-
参数名省略形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort { $0 > $1 }
1.7 trailing 形式闭包
-
闭包可以做其他函数的参数,而且通常都是函数的最后一个参数。但是如果作为参数的这个闭包表达式非常长,那么很有可能会影响函数调用表达式的可读性,这个时候我们就应该使用 trailing 闭包。
-
trailing 闭包和普通闭包的不同之处在于它是一个书写在函数参数括号之外(之后)的闭包表达式,函数会自动将其作为最后一个参数调用。
-
当函数有且仅有一个参数,并该参数是闭包时,不但可以将闭包写在 () 外,还可以省略 ()。
exampleFunction(para1, para2) { 语句组(使用 $0、$1、$2) }
-
trailing 形式闭包示例
let namesArray: Array = ["Jill", "Tim", "Chris"] let names = namesArray.sort() { $0 > $1 }
2、闭包捕获
-
闭包可以在其定义的上下文中捕获常量或变量,即使定义这些常量或变量的原作用域已经不存在,仍然可以在闭包函数体内引用和修改这些常量或变量,这种机制被称为闭包捕获。
-
比如:嵌套函数就可以捕获其父函数的参数以及定义的常量和变量,全局函数可以捕获其上下文中的常量或变量。
func increment(amount: Int) -> (() -> Int) { var total = 0 func incrementAmount() -> Int { // total 是外部函数体内的变量,这里是可以捕获到的 total += amount return total } // 返回的是一个嵌套函数(闭包) return incrementAmount } // 闭包是引用类型,所以 incrementByTen 声明为常量也可以修改 total let incrementByTen = increment(10) incrementByTen() // return 10,incrementByTen 是一个闭包 // 这里是没有改变对 increment 的引用,所以会保存之前的值 incrementByTen() // return 20 incrementByTen() // return 30 let incrementByOne = increment(1) incrementByOne() // return 1,incrementByOne 是一个闭包 incrementByOne() // return 2 incrementByTen() // return 40 incrementByOne() // return 3
3、闭包捕获列表
-
Swift 中可以显式地指定闭包的捕获列表。
-
捕获列表需要尾随
in
关键字,并且紧跟着参数列表。{ [unowned self] (a:A, b:B) -> ReturnType in ... }
-
通过标记
self
为unowned
或者用weak
来打破循环引用,这种方法常常用来修改闭包捕获的这个self
的属性,苹果官方语言指南要求如果闭包和其捕获的对象相互引用,应该使用unowned
,这样能够保证他们会同时被销毁,这大概是为了避免对象被释放后维护weak
引用空指针的开销。{ [unowned self] in ... }
-
捕获列表除了可以设置 self,还可以单独声明引用类型成员变量,这避免了引用
thing1
和thing2
时污染周围代码。{ [thing1 = self.grabThing(), weak thing2 = self.something] in ... }
4、闭包循环引用
- 详情见Swift 循环引用章节。
5、闭包常用关键字
5.1 @escaping 关键字
-
用关键字
@noescape
修饰的闭包称为非逃逸闭包,而用@escaping
修饰的闭包称为逃逸闭包。- 逃逸闭包,表示此闭包还可以被其他闭包调用,比如我们常用的异步操作。
- 非逃逸闭包,传入闭包参数的调用限制在调用的函数体内,对性能有一定的提升,同时将使你能在闭包中隐式地引用
self
。
-
在 Swift 标准库中很多方法,都用了
@noescape
属性,比如Array
对应的方法map
,filter
和reduce
。func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T] func filter(@noescape includeElement: (Self.Generator.Element) -> Bool) -> [Self.Generator.Element] func reduce<T>(initial: T, @noescape combine: (T, Self.Generator.Element) -> T) -> T
-
在方法调用时,方法列表中传入的参数会被拷贝(值类型拷贝内部属性,引用类型拷贝指针),在方法体中操作的实际是拷贝的版本,在方法调用结束时,这些拷贝的版本也会被销毁。而闭包类型的参数和其它类型的参数不太相同。
-
在 Swift 2.2 版本中
- 闭包类型的参数默认会在方法返回后被返回,也就是说你可以在外部保存那些方法参数中的闭包,通常把这种特性称为 “escape” 延迟调用。
- 如果你不需要闭包的 “escape” 特性,只想把闭包作为一段从外部写入的灵活代码,则可以在闭包参数名后加上
@noescape
关键字,这样闭包的代码就和其它参数一样,在方法返回前返回。 - noescape 的参数不能被传递到方法外部。
- 使用
@noescape
的用法是为方法提供灵活的外部代码,并且@noescape
的闭包不会产生循环引用,所以调用本类型中的属性时不需要加self
。
-
在 Swift 3.0 中
- 闭包延迟调用的默认状态发生了反转,noescape 成为了闭包的默认状态,
@noescape
修饰符已经被删除了。 - 现在如果你想使用 “escape” 特性闭包的话,则需要使用
@escaping
显示的声明。 - 虽然 3.0 中的闭包做了修改,不过在 Swift 2.2 中数组中常用的
map、filter
等方法还都接受@noescape
闭包参数。
- 闭包延迟调用的默认状态发生了反转,noescape 成为了闭包的默认状态,
-
-
示例
-
在 Swift 2.2 版本中
struct ClouseTest { var num = 0 var handlerCache: [() -> Void] = [] // escaping 型的闭包,默认 mutating func methodWithClouse(addedNum: Int, completeHandler: () -> Void) { num += addedNum // 把闭包参数加入到缓存数组中,未在方法中执行 handlerCache.append(completeHandler) } // noescape 型的闭包,@ noescape 修饰 func useNum(completeHandler: @noescape (Int) -> Int) -> Int { print(num) return completeHandler(num) } }
var ct = ClouseTest() ct.methodWithClouse(addedNum: 5) { ct.num += 10 } print(ct.num) // num 为 5, 闭包中的代码是延迟调用的,在调用前不会执行 // escaping 型的闭包,默认 ct.handlerCache.first!() // 调用缓存的闭包参数 print(ct.num) // num 的值变为 15 // noescape 型的闭包 let handleReuslt = ct.useNum { num in num + 10 } print(handleReuslt) // 25
-
在 Swift 3.0 中
struct ClouseTest { var num = 0 var handlerCache: [() -> Void] = [] // escaping 型的闭包,@escaping 修饰 mutating func methodWithClouse(addedNum: Int, completeHandler: @escaping () -> Void) { num += addedNum // 把闭包参数加入到缓存数组中,未在方法中执行 handlerCache.append(completeHandler) } // noescape 型的闭包,默认 func useNum(completeHandler: (Int) -> Int) -> Int { print(num) return completeHandler(num) } }
var ct = ClouseTest() ct.methodWithClouse(addedNum: 5) { ct.num += 10 } print(ct.num) // num 为 5, 闭包中的代码是延迟调用的,在调用前不会执行 // escaping 型的闭包 ct.handlerCache.first!() // 调用缓存的闭包参数 print(ct.num) // num 的值变为 15 // noescape 型的闭包,默认 let handleReuslt = ct.useNum { num in num + 10 } print(handleReuslt) // 25
-
5.2 @autoclosure 关键字
-
用关键字
@autoclosure
修饰的闭包称为自动闭包。- 自动闭包,顾名思义是一种自动创建的闭包,用于包装函数参数的表达式,可以说是一种简便语法.
- 自动闭包不接受任何参数,被调用时会返回被包装在其中的表达式的值。
- 自动闭包的好处是让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包,这样你就可以控制代码什么时候执行。
- 含有
autoclosure
特性的声明同时也具有noescape
的特性,即默认是非逃逸闭包,除非传递可选参数escaping
。如果传递了该参数,那么将可以在闭包之外进行操作闭包,形式为@autoclosure(escaping)
。
-
下面一起来看一个简单例子,比如我们有一个方法接受一个闭包,当闭包执行的结果为
true
的时候进行打印。func printIfTrue(predicate: ()-> Bool) { if predicate() { print("the result is true") } }
// 直接调用方法 printIfTrue { () -> Bool in return 2 > 1 } // 闭包在圆括号内 printIfTrue(predicate: { return 2 > 1 }) // 使用尾部闭包方式,闭包体在圆括号之外 printIfTrue() { return 2 > 1 } // 省略 return printIfTrue(predicate: { 2 > 1 }) // 使用尾随闭包 printIfTrue { 2 > 1 }
-
但是不管哪种方式,表达上不太清晰,看起来不舒服,于是
@autoclosure
就登场了,我们可以在参数名前面加上@autoclosure
关键字,这样我们就得到了一个写法简单,表意清楚的式子,被@autoclosure
标注的闭包不再需要写在 "{ }" 中。func printIfTrue(predicate: @autoclosure () -> Bool) { if predicate() { print("the result is true") } }
// 直接进行调用,Swift 将会把 2 > 1 这个表达式自动转换为 () -> Bool。 printIfTrue(predicate: 2 > 1)
-
如果有多个闭包,那么就有优势了,而
@autoclosure
是可以修饰任何位置的参数。func printInformation(predicate1: @autoclosure () -> Bool, predicate2: @autoclosure () -> Bool) { if predicate1() && predicate2() { print("the result is true") } else { print("the result is false") } }
printInformation( predicate1: 3 > 2, predicate2: 4 > 1)
6、闭包风格
-
在 Swift 3.0 之前定义闭包可以用多种格式来表示。比如
// 定义方式 1 let plus: (Int, Int) -> Int = { x in return x.0 + x.1 } print(plus(1, 2))
// 定义方式 2 let plus: (Int, Int) -> Int = {x, y in return x + y } print(plus(1, 2))
- 这两种格式的运算结果相同
- 第一种闭包体中只声明了一个参数,这个参数需要与上下文中的格式相对应,所以
x
代表了一个元组(Int, Int)
。 - 第二种闭包体中传入了两个参数,所以分别对应了上下文中的两个 Int` 类型。
-
实际上这种语法示存在歧义的,Swift 3.0 修复了这种歧义,使用更加严格的方式来响应上下文,参数的表达方式只有一种,即参数列表最外层括号内部的类型。
// 定义方式 1 let plus: ((Int, Int)) -> Int = { x in return x.0 + x.1 } print(plus(1, 2))
// 定义方式 2 let plus: (Int, Int) -> Int = {x, y in return x + y } print(plus(1, 2))