Reference cycle with unowned / weak inside autoclosure

I'm attempting to track down the cause of a reference cycle in Alamofire. I've narrowed it down to this code:

    @discardableResult
    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
        return validate { [unowned self] _, response, data in
            return self.validate(contentType: acceptableContentTypes(), response: response, data: data)
        }
    }

    @discardableResult
    public func validate() -> Self {
        return validate(statusCode: acceptableStatusCodes).validate(contentType: { [weak self] () -> [String] in
            return ["*/*"]
        }())
    }
}

Any self reference in the contentType closure (inline for testing in this case) causes a reference cycle, even if self is never used within the closure. This occurs for both weak and unowned captures. The same closure, without the capture, works fine, as does a direct array value. So where's the strong reference here?

Could it be that in the forwarding method call, your @escaping @autoclosure parameter actually captures self, through the member acceptableContentTypes, strongly?

Edit: The workaround might be to bind (self.)acceptableContentTypes to a local variable before the method call and use that value instead.

There doesn't seem to be an implicit capture anywhere, as the cycle only appears if the contentType closure explicitly captures self. It's the fact that the cycle happens whether we capture unowned or weak, even if self is never used within the closure, that's really confusing.

Can you show me an example where self isn't used in the capture and the reference cycle still exists? Because in the above example, there's an implicit self.

…In this (sub)expression. ^

That expression is irrelevant to the behavior, as acceptableStatusCodes is just an Array. The only thing that makes a difference is the capture seen in the contentType closure. Capturing self into that closure at all triggers the cycle, omitting the capture does not.

Unless it's a global, it appears to be an array-valued member of self, no? And since it is passed as an escaping autoclosure argument, its evaluation is delayed all the way until acceptableContentTypes() in the first method. So there's the implicit capture of self, strongly.

Am I making sense? :smiley:

No, as the status code version of validate is a different code path that does not take an @autoclosure. That validator by itself doesn't not produce a cycle.

Can you try this workaround nevertheless, and see if it changes things?

    @discardableResult
    public func validate() -> Self {
        let workaround = acceptableStatusCodes
        return validate(statusCode: workaround).validate(contentType: { [weak self] () -> [String] in
            return ["*/*"]
        }())
    }
}

I'll go one better: removing validate(statusCode: acceptableStatusCodes) altogether doesn't change the behavior I'm seeing. It only triggers when capturing self into the contentType closure.

No but that's the same thing! When—inside an @autoclosured expression—you declare to capture weak self, that capturing is delayed all the way until that autoclosure argument is evaluated.

Edit: You could better avoid this problem if autoclosure expressions could have their own capture lists (ugly!) or if there existed syntax for passing ("splatting") a closure as the autoclosure argument.

Now that's something I hadn't considered: an implicit capture by the @autoclosure since it needs to pass self into the closure when the outer @autoclosure is called. Let me see if I can trigger a difference.

1 Like

@escaping and @autoclosure together should be read as a warning sign. If it needs to escape, better make it a closure at the call site so that it remains explicit.

It seems like this avoids this issue:

{
    @discardableResult
    public func validate() -> Self {
        let contentType: () -> [String] = { [unowned self] in
            return self.acceptableContentTypes
        }
        return validate(statusCode: self.acceptableStatusCodes).validate(contentType: contentType())
    }
}

As for the usage of @escaping @autoclosure, that seems to be the only way to capture self lazily so that users could validate the received Content-Type matches the sent Accept, or other behaviors.

I'll test this solution more, but I think this will work. Thanks!

2 Likes
Terms of Service

Privacy Policy

Cookie Policy