Robert Robert - 1 month ago 25
Swift Question

How to call non-escaping closure inside a local closure?

I have a function which looks something like this:

func test(closure: () -> ()) {
let localClosure = { closure() }

localClosure()
}


This is only an example and does not fully reflect the problem I encountered, obviously here I could have just called
closure
directly!


It should be clear that in the above code,
closure
cannot escape. However, I get the error:


Closure use of non-escaping parameter 'closure' may allow it to escape


Now, if
localClosure
was escaping in some way, I'd understand this error, but it doesn't escape. I even tried annotating
localClosure
as
@noescape
(even though that attribute is deprecated in Swift 3), and according to the warning I got:


@noescape is the default and is deprecated


If
localClosure
is, by default, non-escaping, then why can't another non-escaping closure go inside it? Or is this a bug/limitation of the compiler?

Answer

"If localClosure is, by default, non-escaping, then why ..."

A closure can only be escaping or non-escaping in the context of a parameter of function type. This means localClosure is neither of these concepts: it's simply a closure living in the scope of test, which means a closure with content which we may use in such a way that it is executed after the lifetime of the call to test.

From the Language Reference - attributes, regarding @escaping:

"Apply this attribute to a parameter’s type in a method or function declaration to indicate that the parameter’s value can be stored for later execution. This means that the value is allowed to outlive the lifetime of the call."

localClosure may be used e.g. to return a closure from test, or to call another method expecting the same type of closure (though @escaping). If we wrap the closure parameter in localClosure, this means we've lost the guarantee that the nonescaping parameter closure supplied to test will never escape, hence the illegality of this operation.

// example 
func test(closure: () -> ()) {
    test2(closureB: closure) // illegal, closureB may escape
}

func test2(closureB: @escaping () -> ()) {
    closure()
}

// another example
func test(closure: () -> ()) -> () -> (){
    let localClosure = { closure() }
        // illegal, may escape via this local closure

    return localClosure
}

The compiler cannot test on by-case basis whether a local closure is used in a way which means its content outlives the call to the function which owns it/scope within which it lives. It can only know that if we wrap a non-escaping closure in such a local closure variable, we can no longer guarantee that the wrapped closure does not escape the lifetime of the function call.


As a side-note, which you possibly already knows given your question: we may naturally mark closure as @escaping if we'd wish to process/wrap it as in the examples above, in which case we're not prompted with an error.

func test(closure: @escaping () -> ()) -> () -> (){
    let localClosure = { closure() }
    return localClosure
}