🤔

Anonymous methods and closures#

Say you have the code:

public void ExampleA()
{
    var actions = new Action[10];
    for (int i = 0; i < 10; i++)
    {
        actions[i] = () => Debug.Log(i);
    }
}

When the logs in actions are invoked after this loop there will be 10 logs of 10.

Why does this occur?#

What the code really looks like when compiled is this:

// Lowered C# code:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int i;

    internal void <ExampleA>b__0()
    {
        Debug.Log(i);
    }
}

public void ExampleA()
{
    Action[] array = new Action[10];
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.i = 0;
    while (<>c__DisplayClass0_.i < 10)
    {
        array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<ExampleA>b__0);
        <>c__DisplayClass0_.i++;
    }
}

// Lowered C# code ⚠️ manually simplified ⚠️:

private sealed class DisplayClass
{
    public int i;

    internal void B() => Debug.Log(i);
}

public void ExampleA()
{
    var array = new Action[10];
    DisplayClass displayClass = new();
    displayClass.i = 0;
    while (displayClass.i < 10)
    {
        array[displayClass.i] = new Action(displayClass.B);
        displayClass.i++;
    }
    // displayClass.i is 10
}

Looking past the fancy symbols (see the manually simplified code), you can see a DisplayClass whose variable i is used as the counter to our loop.
That class is shared across all Actions we create, and i is increased to 10 over the loop's iterations.
When the delegate is invoked after the loop, that value, 10, is used.

Resolution#

Redeclare a local version of the counter, using it in the delegate:

public void ExampleB()
{
    var actions = new Action[10];
    for (int i = 0; i < 10; i++)
    {
        int iLocal = i;
        actions[i] = () => Debug.Log(iLocal);
    }
}

Why does this work?#

The new version of the compiled code looks like this:

// Lowered C# code:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int iLocal;

    internal void <ExampleB>b__0()
    {
        Debug.Log(iLocal);
    }
}

public void ExampleB()
{
    Action[] array = new Action[10];
    int num = 0;
    while (num < 10)
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.iLocal = num;
        array[num] = new Action(<>c__DisplayClass0_.<ExampleB>b__0);
        num++;
    }
}

// Lowered C# code ⚠️ manually simplified ⚠️:

private sealed class DisplayClass
{
    public int iLocal;

    internal void B()
    {
        Debug.Log(iLocal);
    }
}

public void ExampleB()
{
    var array = new Action[10];
    int num = 0;
    while (num < 10)
    {
        // A new DisplayClass is created in the loop, and iLocal is never increased.
        DisplayClass displayClass = new();
        displayClass.iLocal = num;
        array[num] = new Action(displayClass.B);
        num++;
    }
}

As you can see, now a new DisplayClass is created in every iteration of the loop, and the counter is copied to that instance. Meaning only num is increased to 10, and each iLocal is a copy of the state at a different iteration of the loop.


If you use JetBrains Rider you can avoid this issue as it will show the warning:

warning

Captured variable is modified in the outer scope

How to enforce no closures#

This isn't always applicable, but certain methods like UI Toolkit's RegisterCallback take in an args parameter, using generics to capture variables in pooled classes.
If you're using a delegate that expects not to capture variables, you can mark it with the static keyword (as of C# 9.0).

Example#

field.RegisterCallback<ClickEvent, VisualElement>(
    static (_, element) => element.Focus(),
    otherElement
);