闭包概念
Swift 中的闭包是一个在上下文中闭合的独立代码块,可以将 Swift 闭包看作是一个轻量级的函数实例,它可以捕获上下文中的变量和常量,并作为参数传递给其他函数。
闭包的使用场景
在实际应用中,闭包主要用于以下场景:
- 异步操作和回调:如网络请求和延迟执行等。
- 高阶函数:
map
,filter
,reduce
等函数会接收闭包作为操作的参数。 - 事件处理:与用户交互、通知等事件处理相关的回调函数。
- 动画和视图转换:UIKit 和其它 UI 框架中的动画和视图转换操作。
- 资源管理和枚举:例如文件操作、GCD(Grand Central Dispatch)队列和
autoreleasepool
。
一、闭包的主要特点:
-
1.引用类型:闭包是引用类型,像类一样,它们在分配和传递时不复制,而是传递引用。
-
2.捕获上下文值:闭包可以捕获和存储定义它的上下文中的常量和变量,即使它们在闭包创建之后被修改或消失,闭包仍然可以访问它们。
-
3.可被传递给函数:闭包可以通过参数的形式传递给函数,或者作为变量存储在其他闭包中以供后续调用。
二、闭包的格式
闭包格式(in是把闭包的参数或者返回值和 代码逻辑 隔开)
1.无参数无返回值
格式: let 闭包名 = { 代码逻辑 }
2.有参数无返回值格式1: let 闭包名 = {(参数名:参数类型,...) in 代码逻辑 }
格式2: let 闭包名 = {(参数名:参数类型,...) -> Void in 代码逻辑 }
格式3: let 闭包名 = {(参数名:参数类型,...) -> () in 代码逻辑 }
3.有参数有返回值
格式: let 闭包名 = {(参数名:参数类型,...) -> 返回值类型 in 代码逻辑 return 返回值}
三、逃逸闭包(@escaping )与非逃逸闭包(@noescaping)
1.逃逸闭包(Escaping Closures)
概念:逃逸闭包(escaping closure)是 Swift 中闭包的特殊类型,它指的是传递给函数的闭包会在函数返回后执行。
常见的逃逸闭包使用场景有:
- 异步操作,如网络请求、延时执行等。
- 存储 closure,在稍后执行。
- 将闭包传递给其他函数(闭包传递到另一个需要逃逸闭包的函数)。
// 模拟加载数据,在这个示例中,我们使用 @escaping 标注来声明闭包 loadData执行完后的闭包函数是一个逃逸闭包。
func loadData(callBack: @escaping (String) -> ()){
// 子线程异步加载数据
DispatchQueue.global().async {
// 模拟消耗时间
Thread.sleep(forTimeInterval: 2.0)
// 模拟请求回来的数据, 要传递给viewDidLoad中使用
let data = "不想睡"
// 回到主线程, 刷新页面
DispatchQueue.main.async {
// 2. 在得到数据的时候,执行闭包
callBack(data)
}
}
}
//调用
loadData(callBack: closure)
逃逸闭包可能导致循环引用(retain cycle)问题。当闭包在函数之外执行,尤其是在闭包和类实例之间产生相互引用时,需要特别关注内存管理。这时,应使用捕获列表(capture list),指定捕获方式为 weak
或 unowned
。
在swift5.0之后,只 有 闭 包 在 其 他 线 程 执 行 时 才 需 要 添 加 该 关 键 字。
2.非逃逸闭包(@noescaping)
概念:一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
handleData { (data) in
print("闭包结果返回--\(data)--\(Thread.current)")
}
}
func handleData(closure:(Any) -> Void) {
print("函数开始执行--\(Thread.current)")
print("执行了闭包---\(Thread.current)")
closure("4456")
print("函数执行结束---\(Thread.current)")
}
}
执行结果为:
函数开始执行--<_NSMainThread: 0x600002808080>{number = 1, name = main}
执行了闭包---<_NSMainThread: 0x600002808080>{number = 1, name = main}
闭包结果返回--4456--<_NSMainThread: 0x600002808080>{number = 1, name = main}
函数执行结束---<_NSMainThread: 0x600002808080>{number = 1, name = main}
3.为什么要分逃逸闭包和非逃逸闭包
非逃逸闭包不会产生循环引用,而闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用,这样闭包会持有当前对象,容易导致循环引用。
四、自动闭包(@autoclosure
)
概念:
自动闭包(autoclosure)是 Swift 中一种特殊的闭包类型,它可以自动将表达式封装在一个没有参数的闭包中。当函数需要延迟求值或执行特定表达式时,可以用自动闭包将表达式传递给函数。这种类型的闭包在调用时不需要使用括号。
func zidongPrint(_ condition: @autoclosure () -> Bool, message: String) {
if !condition() {
print("Assert Failed: \(message)")
} else {
print("Assert Passed")
}
}
let x = 10
let y = 5
zidongPrint(x > y, message: "x should be greater than y")
因为 condition
参数带有 @autoclosure
标记,所以当我们调用这个函数时,只需传递一个表达式,而不需要显式地创建一个闭包。
在这个示例中,我们传递了表达式 x > y
,而不需要将其封装为一个闭包(如 { x > y }
)。@autoclosure
关键字会自动将表达式封装在一个没有参数的闭包中,在调用 condition()
时执行。
需要注意的是,由于自动闭包的延迟求值特性,可能导致一些不符合预期的情况。例如,在多线程环境中,自动闭包捕获的值在执行时可能已经发生了变化。因此,在使用自动闭包时,请注意程序的逻辑和执行顺序。
五、尾随闭包
尾随闭包就是系统的简写方式,了解即可. 以下两种情况为尾随闭包.
条件1: 定义的函数有且只有一个参数, 并且参数是闭包,函数调用时,( )可以省略。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// 1.0 正常写法
let closure = { (result: String) in
}
loadData(callback: closure)
// 1.1 可以演变成
loadData(callback: { (result: String) in
})
// 2.0 尾随闭包写法: 直接写loadData方法, 按回车,自动联想, 原理是->()和里面的参数名称可以省略, 参数类型也可以省略
loadData { (result) in
}
}
func loadData(callback: (String)->()){
}
条件2: 定义的函数有多个参数, 但是最后一个参数为闭包 , 函数的( )提前关闭.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// 1.0 正常写法
let closure = { (result: String) in
}
loadData(a: 3, b: "8", callback: closure)
// 1.1 可以演变成
loadData(a: 3, b: "8", callback: { (result: String) in
})
// 2.0 尾随闭包写法: 直接写loadData方法, 按回车,自动联想, 原理是->()里面的参数名称可以省略, ()提前闭合, 只留下闭包了,闭包的参数类型也可以省略
loadData(a: 3, b: "8") { (result) in
}
}
func loadData(a: Int, b: String, callback: (String)->()){
}
六、捕获列表(Capture List)
在 Swift 闭包中,捕获列表(Capture list)用于控制闭包体内的变量和常量的捕获行为。当一个闭包捕获一个实例或变量时,闭包存储了对该实例或变量的引用。捕获列表可以让你明确地指定闭包如何捕获外部的值,以避免潜在的循环引用或内存泄漏问题。
捕获列表的语法
捕获列表位于闭包参数列表和返回类型之前,用 []
括起来,并用逗号分隔。
[weak object1, unowned object2, otherValue = someValue] (parameters) -> ReturnType in
// 闭包体
捕获列表的种类
1.weak
:将捕获的引用设为弱引用,允许引用的实例在闭包执行时被释放。当实例被释放时,弱引用变为 nil
。这有助于防止引起循环引用。
2.unowned
:将捕获的引用设为无主引用,表示引用的实例不会在闭包执行期间被释放。在闭包执行时,无主引用始终有值,如果你试图访问已经被释放的无主引用,会导致运行时错误。
3.按值捕获: 可以用赋值表达式捕获变量的当前值,作为常量存储在闭包中。这样,闭包总是使用捕获时的值,而不是捕获变量最新的值。
class Person {
var name: String
init(name: String) {
self.name = name
}
deinit {
print("Person \(name) is being deinitialized.")
}
}
class Task {
var taskDescription: String
lazy var completion: () -> Void = { [weak self] in
print("Task '\(self?.taskDescription ?? "Unknown")' is completed.")
}
init(taskDescription: String) {
self.taskDescription = taskDescription
}
deinit {
print("Task '\(taskDescription)' is being deinitialized.")
}
}
var person: Person? = Person(name: "John Doe")
在上面的例子中,我们使用 [weak self]
捕获列表,使得 Task
类中的 completion
闭包对 self
的引用变成弱引用,以避免循环引用。这样,当 Task
实例被释放时,闭包中的 self
(即 Task
实例)将自动变为 nil
。
捕获列表提供了一种显式地定义闭包捕获行为的方式,有助于防止内存泄漏和解决循环引用问题。
无主引用有什么问题
运行时错误:当试图访问一个已经被释放的无主引用时,程序会触发运行时错误。由于无主引用不会使引用计数增加,你需要确保其引用的实例在需要访问它的整个生命周期内仍然存在。如果实例被意外释放,使用无主引用会导致程序崩溃。
要依据具体情况决定是否使用无主引用。如果你知道其引用的实例在整个生命周期内一定存在,可以使用无主引用。否则,考虑将引用设为弱引用(weak
),允许它在不再需要时被释放并自动变为 nil
,从而避免运行时错误。在使用无主引用时,确保谨慎评估潜在的运行时风险,以防止崩溃和不稳定的应用程序行为。