面试题答案
一键面试闭包捕获变量的原理
在Swift中,闭包可以捕获其定义上下文中的常量和变量。当闭包被定义时,它会捕获所有在其体内引用的外部变量。这意味着即使定义闭包的作用域结束,闭包仍然可以访问和修改这些被捕获的变量。闭包通过引用语义捕获变量,这意味着闭包保存的是对变量的引用,而不是变量的值的副本。例如:
func outerFunction() {
var number = 10
let closure = {
print(number)
}
number = 20
closure() // 输出 20
}
这里闭包closure
捕获了number
变量,当number
的值改变后,闭包中访问到的number
的值也随之改变。
避免闭包循环引用问题
- 弱引用(Weak References):使用
weak
关键字来修饰被闭包捕获的对象,weak
引用不会增加对象的引用计数,从而避免循环引用。例如,在类的实例方法中,如果闭包捕获了self
,可以使用weak
引用self
。 - 无主引用(Unowned References):当你确定被捕获的对象在闭包的生命周期内不会变为
nil
时,可以使用unowned
引用。unowned
引用和weak
类似,也不会增加对象的引用计数,但unowned
引用是一个非可选类型,访问unowned
引用时不会解包。
存在闭包循环引用隐患的Swift代码
class Person {
let name: String
var completionHandler: (() -> Void)?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
func doWork() {
completionHandler = { [unowned self] in // 这里应该用weak而不是unowned,下面修复时会改
print("Work done by \(self.name)")
}
completionHandler?()
}
}
var person: Person? = Person(name: "John")
person?.doWork()
person = nil // Person实例没有被释放,因为循环引用
在这个例子中,Person
类有一个闭包属性completionHandler
,在doWork
方法中,闭包捕获了self
。由于闭包持有self
,而self
又持有闭包,形成了循环引用,导致Person
实例在person
置为nil
时不会被释放。
修复后的代码
class Person {
let name: String
var completionHandler: (() -> Void)?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
func doWork() {
completionHandler = { [weak self] in
guard let self = self else { return }
print("Work done by \(self.name)")
}
completionHandler?()
}
}
var person: Person? = Person(name: "John")
person?.doWork()
person = nil // Person实例被正确释放
在修复后的代码中,使用[weak self]
捕获self
,这样闭包对self
的引用是弱引用,不会增加self
的引用计数,从而避免了循环引用。在闭包内部,使用guard let self = self else { return }
来确保self
在闭包执行时仍然存在。