4

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.

Ádám Bozzay
  • 529
  • 7
  • 19
  • 1
    This looks weird enough to be some kind of bug. I looked at the generated IL and I can't see anything obvious that would cause this, but my guess would be that when it unsubscribes from the event, it doesn't find a matching subscription to unsubscribe for some reason. – Matthew Watson Mar 03 '20 at 11:16
  • It is possible for this to be unintentional. But it would be nice to find an article about this or an issue in the C# Github repo. I tried this with .net Framework 4.7. Right now I think this is because of some class that is generated for the closure. But you said that you looked into the IL and did not see any weird stuff. So I don't know – Ádám Bozzay Mar 03 '20 at 13:42

2 Answers2

1

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);
    }
}

Pethical
  • 1,472
  • 11
  • 18
-2

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://learn.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
    }
Alex - Tin Le
  • 1,982
  • 1
  • 6
  • 11
  • This not an answer to my question. I'm interested in why this happens. Your answer doesn't explain why local functions behave like this. Also, you can't unsubscribe from the event like this. (You can by assigning null to the event, but if the event is in a different class like you suggest, it is not possible.) – Ádám Bozzay Mar 03 '20 at 11:13