Search code examples
c#synchronizationcontext

Is there a technical reason to pass state via a pass-thru callback argument, or capture it via a closure?


While reviewing code last night I found this code. Of note, state is captured via the lambda which executes the generic-typed callback action.

public static void Post<TState>(TState state, Action<TState> callback){

    if(SynchronizationContext.Current is SynchronizationContext currentContext)
        // Use capture-semantics for state
        currentContext.Post(_ => callback(state), null);
    else{
        callback(state);
    }
}

Post however, natively supports pass-thru of state, so the above can also be re-written like this...

public static void Post<TState>(TState state, Action<TState> callback){

    if(SynchronizationContext.Current is SynchronizationContext currentContext)
        // Use pass-thru state, not 'capture'
        currentContext.Post(passedBackState => callback((TState)passedBackState), state);
    else{
        callback(state);
    }
}

My question is simple... in this particular use-case where the callback is defined in the same scope as the state needing to be passed to it, is there any technical benefit and/or down-side to using capture-semantics (the first) over pass-thru (the second)? Personally I like the first because there's only one variable, but I'm asking about a technical difference, if any as both seem to work.


Solution

  • Closures allocate a new object on the heap that needs to be garbage collected. Passing the data through the state parameter doesn't allocate (unless the data is a value type that needs to be boxed).

    These allocations can add up on frequently used methods or long-lived processes like web applications, resulting in wasted CPU cycles for garbage processing and delays. Aggressively removing such allocations is one reason .NET Core is so much faster than .NET Old.

    Roslyn analyzers like the Roslyn Heap Allocation Analyzer highlight such implicit allocations like boxing a value type, closures, using params arrays etc.

    Update

    Rider's Dynamic Program Analysis also highlights allocations due to closures, boxing, enumerators etc.