内部闭包的捕获列表是否需要将“self”重新声明为“weak”或“unowned”?

Do capture lists of inner closures need to redeclare `self` as `weak` or `unowned`?

如果我将闭包传递给这样的函数:

 someFunctionWithTrailingClosure { [weak self] in
     anotherFunctionWithTrailingClosure { [weak self] in 
         self?.doSomething()
     }
 }

如果我在 someFunctionWithTrailingClosure 的捕获列表中将自己声明为 [weak self] 而没有在 anotherFunctionWithTrailingClosure self 的捕获列表中再次将其重新声明为 weak已经成为 Optional 类型,但它是否也成为 weak 参考?

谢谢!

不需要anotherFunctionWithTrailingClosure中的[weak self]

你可以凭经验测试这个:

class Experiment {
    func someFunctionWithTrailingClosure(closure: @escaping () -> Void) {
        print("starting", #function)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing", #function)
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> Void) {
        print("starting", #function)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing", #function)
        }
    }

    func doSomething() {
        print(#function)
    }

    func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            self?.anotherFunctionWithTrailingClosure { // [weak self] in
                self?.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

然后:

func performExperiment() {
    DispatchQueue.global().async {
        let obj = Experiment()

        obj.testCompletionHandlers()

        // sleep long enough for `anotherFunctionWithTrailingClosure` to start, but not yet call its completion handler

        Thread.sleep(forTimeInterval: 1.5)
    }
}

如果这样做,您会看到 doSomething 从未被调用,而 deinitanotherFunctionWithTrailingClosure 调用它的闭包之前被调用。

尽管如此,我可能仍然倾向于在 anotherFunctionWithTrailingClosure 上使用 [weak self] 语法来明确我的意图。

已针对 Swift 4.2 更新:

public class CaptureListExperiment {

    public init() {

    }

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

试试 Playgorund:

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()

TL;DR

尽管在 outer 块中使用一次 [weak self] 没问题(EX1),但如果将此引用更改为强引用(例如 guard let self = self),您在内部块中也需要一个 [weak self] (EX3)。

同时在 内部 块上仅使用一次 [weak self] 通常是一个错误 (EX2_B)。不幸的是,这是重构代码时常犯的错误,并且在发生时很难被发现。


一个好的经验法则是,如果对象在闭包外立即很强,则始终使用 weak

不保留 self 的示例(即通常是 "good" 场景):

// EX1
fn { [weak self] in
  self?.foo() 
}
// EX2
fn { [weak self] in 
  fn2 {
    self?.foo()
  }
}
// self is weak inside fn, thus adding an extra `[weak self]` inside fn2 is unnecessary
// EX3
fn { [weak self] in 
  guard let self = self else { return }
  fn2 { [weak self] in
    self?.foo()
  }
}

确实保留 self 的示例(即通常 "bad" 场景):

// EX1_B
fn {
  self.foo()
}
// fn retains self
// EX2_B
fn {
  fn2 { [weak self] in
    self.foo()
  }
}
// fn retains self (this is a common, hard-to-spot mistake)
// EX3_B
fn { [weak self] in 
  guard let self = self else { return }
  fn2 {
    self.foo()
  }
}
// fn2 retains self

一样,weak有用的主要原因有两个:

  1. 防止保留循环。
  2. 为了防止对象的寿命超过它们应有的寿命。

关于 #2 的更多信息(防止长寿命对象)

中,该函数不保留闭包(在 dispatch_async 之后几乎可以保证在将来的某个时候触发闭包),因此您永远不会结束带有一个保留周期。所以在这种情况下使用 weak 是为了防止 #2 发生。

正如 Hamish 所提到的,在此示例中实际上不需要 weak 来防止保留循环,因为没有保留循环。 weak,在这种情况下,用于防止对象活得比需要的时间长。这完全取决于您的用例,何时考虑一个对象比需要的寿命更长。因此,有时您只想在外部(EX2)使用 weak,而其他时候您想要使用 weak 外部、strong 内部、weak例如内心之舞 (EX3)。

关于#1 的更多信息(防止循环保留)

为了检查保留循环问题,假设一个函数正在存储对块(即堆)的引用,而不是直接引用函数(即堆栈)。很多时候我们不知道 class/function 的内部结构,所以更安全的假设是函数 保留块。

现在您可以使用 weak 外部轻松创建保留循环,并且仅使用 strong 内部 (EX3_B):

public class CaptureListExperiment {

    public init() {

    }

    var _someFunctionWithTrailingClosure: (() -> ())?
    var _anotherFunctionWithTrailingClosure: (() -> ())?

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")
        _someFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._someFunctionWithTrailingClosure!()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")
        _anotherFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._anotherFunctionWithTrailingClosure!()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()

/* Output:

starting someFunctionWithTrailingClosure
starting anotherFunctionWithTrailingClosure
finishing someFunctionWithTrailingClosure
doSomething
finishing anotherFunctionWithTrailingClosure
*/

请注意,deinit 未被调用,因为创建了一个保留循环。

这可以通过删除 strong 引用 (EX2) 来解决:

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        //guard let self = self else { return }
        self?.anotherFunctionWithTrailingClosure { // [weak self] in
            self?.doSomething()
        }
    }
}

或者使用weak/strong/weak舞蹈(EX3):

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        guard let self = self else { return }
        self.anotherFunctionWithTrailingClosure { [weak self] in
            self?.doSomething()
        }
    }
}