Swift - 闭包
定义
- 闭包是一个自包含的函数代码块,可以在代码中被传递和引用。
- 闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。
闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。
- 全局函数是一个有名字但不会捕获任何值的闭包
- 潜逃函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
闭包表达式
闭包表达式的一般形式
{ (parameters) -> return type in
statements
}
以数组的sorted(by:)
方法为例
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
写成一行
names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2})
根据上下文推断类型
sorted(by:)
方法被一个字符串数组调用,Swift 可以推断其参数和返回值的类型,因此其参数必须是(String, String) -> Bool
- 这意味着
(String, String)
和Bool
类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->
)和围绕在参数周围的括号也可以被省略:
names.sorted(by: { s1, s2 in return s1 > s2})
单表达式闭包的隐式返回
- 单行表达式闭包可以通过省略
return
关键字来隐式返回单行表达式的结果
names.sorted(by: { s1, s2 in s1 > s2})
参数名称缩写
- Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过
$0
,$1
,$2
来顺序调用闭包的参数,以此类推。 - 闭包接受的参数的数量取决于所使用的缩写参数的最大编号。
in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
names.sorted(by: {s1 > s2})
运算符方法
- Swift 的
String
类型定义了关于大于号(>
)的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。而这正好与sorted(by:)
方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
names.sorted(by: >)
尾随闭包
尾随闭包是一种特殊的闭包语法,它可以在函数调用的括号外部以简洁的方式提供闭包作为函数的最后一个参数。
使用尾随闭包的优势在于增加了代码的可读性和简洁性。当闭包作为函数的最后一个参数时,将闭包放在括号外部,可以使函数调用更加清晰,更接近于自然语言的阅读顺序。
func calculate(a: Int, b: Int, closure: (Int, Int) -> Int) {
let result = closure(a, b)
print(result)
}
// 调用函数时使用尾随闭包
calculate(a: 5, b: 3) { (x, y) -> Int in
return x + y
}
// 如果闭包只包含一个表达式,可以省略 return 关键字
calculate(a: 5, b: 3) { (x, y) in
x + y
}
// 省略参数的类型和括号
calculate(a: 5, b: 3) { x, y in
x + y
}
// 使用 $0, $1 等缩写形式代替参数名
calculate(a: 5, b: 3) {
$0 + $1
}
如果一个函数接受多个闭包,需要省略第一个尾随闭包的参数标签,并为其余尾随闭包添加标签。
值捕获
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
可以捕获值的闭包最简单的形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
注意:
如果将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员捕获了该实例,将会造成一个循环引用。
捕获列表
默认情况下,闭包会捕获附近作用域中的常量和变量,并使用强引用指向它们。你可以通过一个捕获列表来显示指定它的捕获行为。
捕获列表在参数列表之前,由中括号括起来,里面是由逗号分隔的一系列表达式。一旦使用了捕获列表,就必须使用in
关键字,即使省略了参数名、参数类型和返回类型。
捕获列表中的项会在闭包创建时被初始化。每一项都会用闭包附近作用域中的同名常量或者变量的值初始化。例如下面的代码实例中,捕获列表包含a
而不包含b
,这将导致这两个变量有不同的行为。
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}
a = 10
b = 10
closure()
// 打印“0 10”
如果捕获列表中的值是类类型,可以使用weak
或unowned
来修饰它,闭包会分别用弱引用、无主引用来捕获该值:
myFunction { print(self.title) } // 隐式强引用捕获
myFunction { [self] in print(self.title) } // 显式强引用捕获
myFunction { [weak self] in print(self!.title) } // 弱引用捕获
myFunction { [unowned self] in print(self.title) } // 无主引用捕获
在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照制定的引用类型来捕获表达式的值:
// 以弱引用捕获 self.parent 并赋值给 parent
myFunction { [weak parent = self.parent] in print(parent!.title) }
解决闭包的循环强引用
在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无助引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。
使用规则
在闭包和捕获的实例总是互相引用并且同时销毁时,将闭包内的捕获定义为无主引用
相反,在被捕获的引用可能会变为nil,将闭包内的捕获定义为弱引用,弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在
注意
如果被捕获的实例绝对不会变为
nil
,应该使用无主引用,而不是弱引用。
闭包是引用类型
无论你将函数和闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用
逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数之后才被执行,称该闭包从函数中逃逸。
在参数名之前标注@escaping
指明这个闭包是允许逃逸出这个函数。
一种能使闭包"逃逸"出函数的方法是,将这个闭包包存在一个函数外部定义的变量中。例子:很多异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。这种情况下,闭包需要"逃逸"出函数,因为闭包需要在函数返回之后被调用:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
注意
将一个闭包标记为
@escaping
意味着你必须在闭包中显式地引用self
自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”
总结
Swift 的闭包有以下几个主要的知识点:
- 闭包表达式(Closure Expressions):闭包表达式是一种在简短的几行代码中完成自包含的功能代码块。比如数组的排序方法
sorted(by:)
。 - 尾随闭包(Trailing Closures):如果你需要将一个很长的闭包表达式作为一个函数的最后一个参数,使用尾随闭包是很有用的。尾随闭包是一个书写在函数或方法的括号之后的闭包表达式。
- 值捕获(Value Capturing):闭包可以在其定义的上下文中捕获和存储任何常量和变量的引用。这就是所谓的闭包的值捕获特性。
- 闭包是引用类型(Closures Are Reference Types):无论你将函数/方法或闭包赋值给一个常量还是变量,你实际上都是将引用赋值给了一个常量或变量。如果你对这个引用进行了修改,那么它将影响原始数据。
- 逃逸闭包(Escaping Closures):一个闭包可以“逃逸”出被定义的函数并在函数返回后被调用。逃逸闭包通常存储在定义了该闭包的函数的外部。
- 自动闭包(Autoclosures):自动闭包能让你延迟处理,因为代码段不会被执行直到你调用这个闭包。自动闭包很有用,用来包装那些需要被延迟执行的代码。
Swift 闭包和OC Block
相似点:
- 都是可以捕获和存储其所在上下文的变量和常量的引用的代码块。
- 都可以作为参数传递给函数或方法,或者作为函数或方法的返回值。
- 都可以在代码块中定义局部变量和常量。
- 都可以访问其被创建时所处的上下文环境。
区别:
- 语法:Swift 的闭包语法更简洁明了,使用大括号
{}
来定义闭包,而 Objective-C 的 Block 语法相对复杂,使用^
符号和大括号^{}
来定义 Block。 - 内存管理:Objective-C 的 Block 对捕获的对象默认使用强引用,需要注意避免循环引用;而 Swift 的闭包对捕获的变量默认使用强引用,但通过使用捕获列表(capture list)可以实现对捕获变量的弱引用或无引用。
- 类型推断:Swift 的闭包对于参数和返回值的类型具有类型推断的能力,可以省略类型注解;而 Objective-C 的 Block 需要明确指定参数和返回值的类型。
- 逃逸闭包:Swift 可以将闭包标记为
@escaping
,表示闭包可能会在函数返回之后才被调用;而 Objective-C 的 Block 默认是可以在函数返回后被调用的。
链接:https://juejin.cn/post/7250756790239969340
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。