我在这个嵌套函数中捕获自我吗?编译器不会发出警告

Am I capturing self in this nested function? The compiler does not fire a warning

我找不到关于此的任何官方文档,并且有不同的意见。

下面的情况,一切正常

final class MyVC: UIViewController {
    
    var space: Space!
    
    private let tableView = MenuCategoriesTableView()
    
    private let tableViewHandler = MenuCategoriesTableViewHandler()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        tableView.dataSource = tableViewHandler
        tableView.delegate = tableViewHandler
        
        tableViewHandler.didSelectRow = { [unowned self] option in
            let category = option.makeCategory()
            if category.items.count > 0 {
                let controller = MenuItemsViewController()
                controller.title = option.rawValue
                controller.space = self.space
                self.show(controller, sender: self)
            } else {
                // whatever
            }
        }
    }
}

但是,如果我进行以下更改,我就不再需要使用 unowned self,但我仍然担心捕获自我。我应该担心吗?如果不是,为什么?

final class MyVC: UIViewController {
    
    ...etc...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        ...etc...
        
        func categorySelected(_ option: MenuOption, _ category: MenuCategory) {
            let controller = MenuItemsViewController()
            controller.title = option.rawValue
            controller.space = space
            show(controller, sender: self)
        }
        
        tableViewHandler.didSelectRow = { option in
            let category = option.makeCategory()
            if category.items.count > 0 {
                categorySelected(option, category)
            } else {
                // whatever
            }
        }
    }
}

当您将闭包分配给 tableViewHandler.didSelectRow 时,您分配给它并保留闭包捕获的任何内容。

self 正在保留 tableViewHandler.

因此,危险在于您将在闭包中引用 self。如果你这样做,那就是一个保留周期。

这可能不是因为明确引用了 self。任何提及 属性 或 self 的方法都是对 self.

的隐含引用

好吧,既然如此,让我们检查闭包。

您没有在闭包主体中隐式或显式提及 self。但是,您确实调用了本地方法 categorySelected。因此,你捕获这个方法。

并且categorySelected确实提到了self。因此,捕获self(因为每个函数都是闭包)。

因此存在潜在的保留循环,您应该继续说 unowned self 以防止保留循环。

(我认为编译器不能通过警告保留循环来帮助你;太复杂了。这是一个你只能通过人类理性来解决的问题。)

我做了一些调查,如果你使用引用 self.

的内部函数,你确实会得到一个保留周期

这是一个例子:

typealias ClosureType = () -> ()
class Test {
    var closure:ClosureType?
    let value = 42
    
    func setClosure1() {
        self.closure = {
            print ("from setClosure1")
        }
    }
    
    func setClosure2() {
        self.closure = {
            [unowned self] in
            let v = self.value
            print ("from setClosure2 - value: \(v)")
        }
    }
    
    func setClosure3() {
        func innerFunc() {
            // [unowned self] in  // not allowed, compile error (sometimes even crashes)
            let v = value
            print ("value: \(v)")
        }
        self.closure = {
            [unowned self] in   // compiler warning: "Capture [self] was never used"
            print ("from setClosure3")
            innerFunc()
        }
    }

    deinit {
        print ("Deinit")
    }
}

如果使用setClosure1(平凡)或setClosure2(捕获子句),则不会发生保留循环:

if (1==1) {
    let t = Test()
    t.setClosure1()
    t.closure?()
}  // deinit called here

但是如果你调用setClosure3deinit将不会被调用:

if (1==1) {
    let t = Test()
    t.setClosure3()
    t.closure?()
}  // deinit NOT called here

没有直接的方法解决这个问题;如您所见,在内部函数中使用 [unowned self] 会导致编译器错误,而在 setClosure3 中使用会导致警告。

不过,有一种方法可以解决这个问题 - 您可以使用第二个闭包,而不是使用内部函数,您可以在其中 指定 [unowned self] 捕获子句:

    func setClosure4() {
        let innerClosure:ClosureType = {
            [unowned self] in
            let v = self.value
            print ("value: \(v)")
        }
        self.closure = {
            print ("from setClosure4")
            innerClosure()
        }
    }

// ...

if (1==1) {
    let t = Test()
    t.setClosure4()
    t.closure?()
}  // deinit called here

结论:Swift 应该允许在内部函数中使用捕获子句。