Swift Memory Management and storing func

by Aaron Hayman in


I ran into an interesting problem in my Swift development, namely, that while it's really convenient to pass class functions as arguments to another class function (aka: very functional programming like), you probably should never do it.

So, coming from objective-c and blocks I would normally do something like this:

__weak id _self = self;
iVar.someBlock = ^{
    [_self doSomething];
};

Of course, the iVar class would copy the block and store it. No retain cycle exists because I've capture __weak id _self.

In Swift, I'm a little less certain, especially since I can pass class functions/methods. So, let's say in the iVar class I have:

class iVarClass {
    var callBack:() -> ()?
    func jumpUpAndDown(){
        //Weeeeeee!
        self.callBack?()
    }
}

Now in my "main" class I have an instance variable of the above class and I do:

class mainClass {
    var iVar: iVarClass
    init(iVar:iVarClass){
        self.iVar = iVar
        iVar.callback = self.doSomething
    }
    func doSomething(){
      self.iVar.jumpUpAndDown?()
    }
}

Do we have a retain cycle here? I would think so, and I think that perhaps I need to make the callback weak:

weak var callBack:() -> ()?

Of course, I could do something like this in the main class:

init(iVar:iVarClass){
        self.iVar = iVar
        weak var _self = self
        iVar.callback = {
            _self?.doSomething()
        }
    }

But it's so nice to be able to pass class functions as arguments! Also, if I do need to make the callback weak, then I think I would loose the ability to assign it a closure (because after the assignment, the closure would be released from memory with only one weak reference).

Also, notice how the onus for memory management responsibility is on the receiver now instead of the assigner, but since the receiver cannot know the source of the assignment it can't really be held responsible. In other words, there must now be a implicit contract between the receiver and the assigner on what kind of function is to be passed, which is fragile and not-recommended. When the assigner is responsible, it can take steps to ensure there's no retain cycle, but the receiver cannot take such steps.

This makes me think that we should never pass a class function to another object. It's too dangerous. You can't know how the receiver will store/use it.

However, I might be missing something here. It's possible Swift does some magic I'm unaware of. I've posted a SO question, and perhaps I'll get an answer but until then I think I'll need to avoid assigning class functions to other class functions. Damn, though, it's convenient if I could...

Update

So I'd forgotten one trick Apple has provided us:

init(iVar:iVarClass){
    self.iVar = iVar
    iVar.callback = { [unowned self] in
        self.doSomething()
    }
}

This is nicer than having to explicitly declare the weak variable, but not nearly as nice as being able to directly assign to the call back a class function. Also, you can use weak instead of unowned. If you use weak, the enclosed variable self will be optional. With unowned the enclosed variable is deallocated along with it's referenced class. Apple recommends using unowned whenever possible presumably for performance reasons. What results is that while you haven't actually created a retain cycle, captured self will remain so long as the receiver remains, which it will so long as it has any other referenced to it other than captured self.

Follow that? Essentially, Apple is ensuring that they both are deallocated at the same time. Aside from the reference to each other, all other references to either object will result in both objects remaining in memory. Kind of neat, actually.

Nope I was wrong, and probably foolishly optimistic about Apple's language. I misread Apple's docs and thought that Swift ensured unowned variable persistence, where what they actually say is that if you know the unowned variable will persist then you use unowned. They still suggest you use it instead of weak, but if the captured reference is referenced after it is dealloced, then an exception will be thrown. So essentially, unowned is a dangling pointer with a predictable error (as opposed to undefined behavior).