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

25

如果我将一个闭包传递给一个函数,像这样:

 someFunctionWithTrailingClosure { [weak self] in
     anotherFunctionWithTrailingClosure { [weak self] in 
         self?.doSomething()
     }
 }
如果我在someFunctionWithTrailingClosure的捕获列表中声明自己为[weak self],但在anotherFunctionWithTrailingClosure的捕获列表中未重新声明它为weak,那么self已经变成了一个可选类型,但它也会变成一个弱引用吗?
谢谢!

这可以通过经验轻松验证。是的,在anotherFunctionWithTrailingClosure的闭包中,self既是可选的又是weak的。 - Rob
1
可选部分很容易验证,但我如何验证它也是弱的? - Nirma
3个回答

25

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从未被调用,并且在anotherFunctionWithTrailingClosure调用其闭包之前调用了deinit

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

12

简述:

[weak self]一般用于闭包外部对象的强引用,如果在闭包内部也要使用该对象,则需要在闭包内使用[weak self]。如果只在内部使用[weak self],通常是错误的。建议开发者在闭包外部对象强引用的情况下始终使用weak

例子:

// 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的示例(通常是“不好”的情况):

// 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
正如Hamish指出的那样,使用weak有两个主要原因:
  1. 防止保留循环。
  2. 防止对象的生存时间过长。

更多关于第二点(防止长期存活的对象)

Rob的示例中,此函数未保留闭包(超出了几乎可以保证在将来的某个时间点触发闭包的dispatch_async之外),因此您永远不会遇到保留循环。 因此,在这种情况下使用weak是为了防止#2发生。
正如Hamish所提到的,实际上不需要在此示例中使用weak来防止保留循环,因为没有保留循环。 在这种情况下,weak的使用是为了防止对象存在比所需的时间更长。 完全取决于您的用例何时将对象视为比所需的时间更长。 因此,有时您只想在外部使用weak(EX2),而其他时候您将希望使用weak外、strong内、weak内部舞蹈(EX3),例如。

更多关于第一点(防止保留循环)

为了研究保留循环问题,假设一个函数存储了对块(即堆)的引用而不是直接引用函数(即栈)。许多时候我们不知道类/函数的内部情况,因此更安全的做法是假定该函数正在保留该块。
现在您可以仅使用strong内部(EX3_B)轻松创建保留循环。 但通过使用weak外部和strong内部,您可以避免这种情况。
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()
        }
    }
}

或者使用弱/强/弱舞蹈(示例3):

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

嗨,感谢您提供详细信息!非常有帮助!但是,我对您提到的“EX2_B”中的“fn retains self(这是一个常见的难以发现的错误)”感到困惑。但是,在“EX2_B”中,我只观察到self在fn2中使用而不是fn中。您介意再解释一下吗?谢谢。 - Cheok Yan Cheng

2

更新至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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接