Checking if a closure is escaping or nonescaping?

Hi,

Is escaping part of the closure's type? Is so, how can I check if a closure is escaping vs nonescaping?


typealias CaptureClosure = () -> ()
func testEscaping (
    nonEscapingByDefault:CaptureClosure, escapingByDefault:CaptureClosure? 
) {
    //NONESCAPING
    print(type(of: nonEscapingByDefault)) // // How can I see if this is nonescaping?
    
    //ESCAPING Optional Clousure 
    if let escapingByDefault = escapingByDefault {
        let _ = Mirror(reflecting: escapingByDefault) // I know is escaping because no error here
        print(type(of: escapingByDefault)) // How can I see if this is escaping?
    }
    
    // ESCAPING LOCAL Closure
    let escapingLocalClosure:CaptureClosure = {}
    let _ = Mirror(reflecting: escapingLocalClosure) // I know is escaping because no error here
    print(type(of: escapingLocalClosure)) // How can I see if this is escaping?
    
    // ESCAPING local func
    func escapingLocalFunc() {}
    let _ = Mirror(reflecting: escapingLocalFunc) // I know is escaping because no error here
    print(type(of: escapingLocalFunc)) // How can I see if this is escaping?
}
testEscaping(nonEscapingByDefault: {}, escapingByDefault: {})


It is not. If it was part of the type, I would expect this code to work, but it doesn't:

func identity<T>(_ arg: T) -> T { arg }
func test(arg: () -> Void) {
    identity(arg) // Converting non-escaping parameter 'arg' to generic parameter 'T' may allow it to escape
}

The simplest way is to attempt to escape it!

func escapingArgument<T, U>(_ arg: @escaping (T) -> U) { }
func testEscaping (
    nonEscapingByDefault:CaptureClosure, escapingByDefault:CaptureClosure?
) {
    escapingArgument(nonEscapingByDefault) // Passing non-escaping parameter 'nonEscapingByDefault' to function expecting an @escaping closure
    escapingArgument(escapingByDefault!) // works
}

Isn't that a clear indication though that @escaping is a part of the type?

It generally feels like @escaping has quite normal subtyping effect: not all closures/functions are escaping, but all escaping closures/functions are still closures/functions.

At runtime? It might be theoretically possible to do so since escaped closures carry around their context which would make it be a non-null pointer, but that would require Swift to have a type that marks all functions in general, which we can't denote as of now. That is, we would need

func isEscaping<T: IsFunction>(_ f: T) -> Bool

but we don't have a "function protocol" like IsFunction above; we can only write tons of overloads for function types of all possible parameter/return tuples, like () -> (), (A) -> (), () -> A, (A, B) -> (), (A) -> (B), () -> (A, B) etc.

if escaping was part of the closure type, then T in identity function would become @nonescaping () -> () and there would be no problem with passing nonEscapingByDefault to it

identity function works with normal subtyping

class Closure {}
class EscapingClosure: Closure {}

func identity<T>(_ arg: T) -> T { arg }

func test(arg: Closure) {
    identity(arg) // compiles without an error
}

Generic parameters are always considered @escaping, the compiler gives a note about this alongside the error you mention. So, in fact, any generic parameter implicitly receives a constraint as though it was

func identity<T: Escaping>(_ arg: T) -> T { arg }

— because it's impossible to know for an arbitrary function generic over T if it stores the argument somewhere or not, the strongest constraint is assumed.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy