Passing a function argument by referencing the function by name is very idiomatic, e.g.
func increment(_ x: Int) -> Int {
x + 1
}
[1, 2, 3].map(increment)
Thus it is natural to do the same for methods in a class.
class Foo {
private func increment(_ x: Int) -> Int {
x + 1
}
func bar(_ xs: [Int]) -> [Int] {
xs.map(increment)
}
}
The above code works fine and there is not an issue in this case. But there is a subtle difference between the two examples. When you pass a method instead of a function, it creates a strong reference to self
. This can be a problem in escaping closures which have the same lifecycle as whatever class, as then you can very easily create a reference cycle by accident. In my experience this comes up often when using RxSwift/Combine:
class MyViewController: UIViewController {
let button = UIButton()
var count = 0
let label = UILabel()
let subject = PassthroughSubject<Void, Never>()
var bag = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
subject
.sink(receiveValue: increment)
.store(in: &bag)
}
func increment() {
count += 1
label.text = "\(count)"
}
@objc func tapped() {
subject.send(())
}
}
The problematic line is .sink(receiveValue: increment)
, which looks really clean but creates a strong reference to self, it should instead be .sink(receiveValue: { [weak self] in self?.increment })
. Once you know about this pitfall, you will always write the latter, but I think the compiler could help.
I'm proposing some new syntax for when passing a method as an escaping closure argument. The existing syntax could continue to build, but Swift could emit a warning that a strong reference is created and suggest a fix. This would not be required on anything other than reference types (as I think it's a problem for actors too now?). I could see something like this:
.sink(receiveValue: weak increment)
.sink(receiveValue: weak self.increment)
.sink(receiveValue: strong increment)
.sink(receiveValue: strong increment)
.sink(receiveValue: increment) // warning, passing a method as an escaping closure, this creates a strong reference to self, use `strong` or `weak` to silence this warning.
weak
could essentially just translate into .sink(receiveValue: { [weak self] in self?.increment })
and strong would just pass the method as-is.
The only real drawback I see is this creates more implicit behavior that if the instance of your class goes out of memory, then your method won't be called, but I think this is preferable to accidentally creating a reference cycle. Additionally it could emit a lot of warnings for existing projects but it seems best to explicitly mark each usage as strong/weak.
Definitely not a language designer though so I'm curious to hear your thoughts.