简体   繁体   中英

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

I can't find any official documentation on this and there's mixed opinions out there.

In the following situation, all is well.

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
            }
        }
    }
}

However, if I make the following change, I no longer need to use unowned self , but I'm still concerned about capture self. Should I be concerned? If not, why?

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
            }
        }
    }
}

When you assign a closure to tableViewHandler.didSelectRow , you assign to it and retain whatever that closure captures.

self is retaining tableViewHandler .

Therefore, the danger is that you will refer to self within the closure. If you do, that's a retain cycle.

And this might not be due to referring to self explicitly. Any mention of a property or method of self is an implicit reference to self .


Okay, so with that out of the way, let's examine the closure.

You do not mention self implicitly or explicitly in the body of the closure. However, you do call a local method, categorySelected . Therefore, you capture this method.

And categorySelected does mention self . Therefore, it captures self (because every function is a closure).

Thus there is a potential retain cycle and you should continue to say unowned self to prevent a retain cycle.

(I presume that the compiler can't help you here by warning of the retain cycle; there's too much complexity. It's a problem you just have to solve by human reason.)

I did some investigations, and indeed you get a retain cycle if you use an inner function that references self .

Here is an example:

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")
    }
}

If you use setClosure1 (trivial) or setClosure2 (capture clause), no retain cycle occurs:

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

but if you call setClosure3 , deinit will not be called:

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

There is no direct way to solve this problem; as you can see, using [unowned self] in the inner function results in an compiler error, and using it in setClosure3 results in a warning.

Nevertheless, there is a way to get around this issue - instead of using an inner function, you can use a second closure, in which you can specify the [unowned self] capture clause:

    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

Conclusion: Swift should allow capture clauses in innner functions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM