C# local function with closure as event handler: why it works like this?

Viewed 1080

I encountered a behavior recently that I don't understand. I have a local function that captures variables/parameters from the enclosing method. In this case it seems to me that every time I call the enclosing method, a new "instance" of the local function will be created. This behavior is easy to see in the code below.

Can you explain to me why a local function behaves like this?

Neither VS or Resharper gives me any warning for doing this, but this is easy to miss and can cause difficult to find bugs.

public class LocalFunctionTest
    {
        public static void Main(string[] args)
        {
            var localFunctionTest = new LocalFunctionTest();
            localFunctionTest.UnsubscribeSubscribe(1);
            localFunctionTest.UnsubscribeSubscribe(10);
            localFunctionTest.UnsubscribeSubscribe(100);
            Console.WriteLine(localFunctionTest.EventWithoutClosure?.GetInvocationList().Length ?? 0); //1
            Console.WriteLine(localFunctionTest.EventWithClosure?.GetInvocationList().Length ?? 0); //3
        }

        private void UnsubscribeSubscribe(int someParam)
        {
            void EventHandlerWithoutClosure(object sender, EventArgs args)
            {
            }

            //Local function that captures a variable/parameter
            void EventHandlerWithClosure(object sender, EventArgs args)
            {
                someParam++;
            }

            //Using local functions as event handlers
            EventWithoutClosure -= EventHandlerWithoutClosure;
            EventWithoutClosure += EventHandlerWithoutClosure;
            EventWithClosure -= EventHandlerWithClosure;
            EventWithClosure += EventHandlerWithClosure;
        }

        private event EventHandler EventWithoutClosure;
        private event EventHandler EventWithClosure;
    }

Some alternatives to the code above would be:

  • If you create a local variable inside the local function and assign the parameter to it, is still behaves like a closure.

  • If you create a field and assign the parameter to it in the enclosing method, and access the field in the local function, it won't behave like a closure.

2 Answers

The compiler needs save your parameter (someParam) value, because you increment it later in your local function. So it can't use singleton in this case.

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Threading;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class LocalFunctionTest
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public int someParam;

        private void <UnsubscribeSubscribe>g__EventHandlerWithClosure|1(object sender, EventArgs args)
        {
            someParam++;
        }
    }

    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        private void <UnsubscribeSubscribe>g__EventHandlerWithoutClosure|1_0(object sender, EventArgs args)
        {
        }
    }

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private EventHandler m_EventWithoutClosure;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private EventHandler m_EventWithClosure;

    private event EventHandler EventWithoutClosure
    {
        [CompilerGenerated]
        add
        {
            EventHandler eventHandler = this.m_EventWithoutClosure;
            EventHandler eventHandler2;
            do
            {
                eventHandler2 = eventHandler;
                EventHandler value2 = (EventHandler)Delegate.Combine(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.m_EventWithoutClosure, value2, eventHandler2);
            }
            while ((object)eventHandler != eventHandler2);
        }
        [CompilerGenerated]
        remove
        {
            EventHandler eventHandler = this.m_EventWithoutClosure;
            EventHandler eventHandler2;
            do
            {
                eventHandler2 = eventHandler;
                EventHandler value2 = (EventHandler)Delegate.Remove(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.m_EventWithoutClosure, value2, eventHandler2);
            }
            while ((object)eventHandler != eventHandler2);
        }
    }

    private event EventHandler EventWithClosure
    {
        [CompilerGenerated]
        add
        {
            EventHandler eventHandler = this.m_EventWithClosure;
            EventHandler eventHandler2;
            do
            {
                eventHandler2 = eventHandler;
                EventHandler value2 = (EventHandler)Delegate.Combine(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.m_EventWithClosure, value2, eventHandler2);
            }
            while ((object)eventHandler != eventHandler2);
        }
        [CompilerGenerated]
        remove
        {
            EventHandler eventHandler = this.m_EventWithClosure;
            EventHandler eventHandler2;
            do
            {
                eventHandler2 = eventHandler;
                EventHandler value2 = (EventHandler)Delegate.Remove(eventHandler2, value);
                eventHandler = Interlocked.CompareExchange(ref this.m_EventWithClosure, value2, eventHandler2);
            }
            while ((object)eventHandler != eventHandler2);
        }
    }

    public static void Main(string[] args)
    {
        LocalFunctionTest localFunctionTest = new LocalFunctionTest();
        localFunctionTest.UnsubscribeSubscribe(1);
        localFunctionTest.UnsubscribeSubscribe(10);
        localFunctionTest.UnsubscribeSubscribe(100);
        EventHandler eventWithoutClosure = localFunctionTest.m_EventWithoutClosure;
        Console.WriteLine((eventWithoutClosure != null) ? eventWithoutClosure.GetInvocationList().Length : 0);
        EventHandler eventWithClosure = localFunctionTest.m_EventWithClosure;
        Console.WriteLine((eventWithClosure != null) ? eventWithClosure.GetInvocationList().Length : 0);
    }

    private void UnsubscribeSubscribe(int someParam)
    {
        <>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
        <>c__DisplayClass1_.someParam = someParam;
        EventWithoutClosure -= new EventHandler(<>c.<>9.<UnsubscribeSubscribe>g__EventHandlerWithoutClosure|1_0);
        EventWithoutClosure += new EventHandler(<>c.<>9.<UnsubscribeSubscribe>g__EventHandlerWithoutClosure|1_0);
        EventWithClosure -= new EventHandler(<>c__DisplayClass1_.<UnsubscribeSubscribe>g__EventHandlerWithClosure|1);
        EventWithClosure += new EventHandler(<>c__DisplayClass1_.<UnsubscribeSubscribe>g__EventHandlerWithClosure|1);
    }
}

It's because you don't use it in correct way.

Normally, the event is in 1 class and handlers are registered from other classes (instances). If you use local function, why don't you just call the code directly without using event handler?

https://docs.microsoft.com/en-us/dotnet/api/system.eventhandler-1?view=netcore-3.1

If you still insists to do that, you should do

    public static void Main(string[] args)
    {
        var localFunctionTest = new LocalFunctionTest();
        //localFunctionTest.UnsubscribeSubscribe(1);
        //localFunctionTest.UnsubscribeSubscribe(10);
        //localFunctionTest.UnsubscribeSubscribe(100);

        localFunctionTest.EventWithoutClosure += (object sender, EventArgs args) =>
        {
            var test = 1; // dosomething;
        };
        localFunctionTest.EventWithClosure += (object sender, EventArgs args) =>
        {
            var test = 1; // dosomething;
        };

        Console.WriteLine(localFunctionTest.EventWithoutClosure?.GetInvocationList().Length ?? 0); //1
        Console.WriteLine(localFunctionTest.EventWithClosure?.GetInvocationList().Length ?? 0); //1
    }
Related