Capturing a variable in a closure behaves differently in Unity

I have problems capturing a variable in a closure.
My code looks like this:

BetterList<Action> actions = new BetterList<Action>();
for (int i = 0; i < 7; i++) {
    
    var innerCopy = i;
    Action action = new Action(delegate {});
    action += delegate () {
        
        Debug.Log("I am action number " + innerCopy);
    };
    actions.Add(action);
}
foreach (Action action in actions) {
    
    action();
}

The last loop will output “I am action number 6” seven times.

I don’t really understand why innerCopy isn’t captured by the closure! Especially because this code I presented is exactly the same as an accepted answer to the same problem:

So I am a bit confused, did I do something wrong, or is there some kind of bug in the Unity/.net environment that causes this behavior?

EDIT:
Thanks to Bunny83, I tried my code in a new empty project, and it worked flawlessly.
But now I discovered how to reproduce my error:

IEnumerator ProduceError() {
    BetterList<Action> actions = new BetterList<Action>();
    for (int i = 0; i < 7; i++) {
        var innerCopy = i;
        Action action = new Action(delegate {});
        action += delegate () {
            Debug.Log("I am action number " + innerCopy);
        };
        actions.Add(action);
    }
    foreach (Action action in actions) {
        action();
    }
    yield return new WaitForEndOfFrame();
}
void Awake () {
    StartCoroutine(ProduceError());
}

Somehow the coroutine screws the closures up.

It works perfectly for me. I get the text 7 times with a the value goes from 0 up to 6. Is there a reason why you create a multicast delegate with an empty function and your actual function? Anyways, it works as it is. Even when i put an additional debug log in your empty delegate i get all values as you would expect.

edit Note: I used the “normal” generic List, maybe that’s the root of your problem…

second edit:

Well, doing this in a coroutine would be of course something totally different :wink:

It’s not that easy to spot but if you understand what a coroutine is it makes totally sense :wink:

An iterator (or generator) function is not a usual function. The code will be packed into a class. All local variables (as well as parameters) will become member variables of this generated class. The class implements the IEnumerator interface and all your actual code goes into a statemachine like construct in the MoveNext method of that class.

That’s why your closure will close around the member variable of the coroutine which will be the same each iteration.

This will work because innerCopy isn’t a part of the coroutine:

IEnumerator ProduceError()
{
    List<Action> actions = new List<Action>();
    for (int i = 0; i < 7; i++)
    {
        System.Action doIt = ()=>{
            var innerCopy = i;
            Action action = new Action(delegate {});
            action += delegate () {
                Debug.Log("I am action number " + innerCopy);
            };
            actions.Add(action);
        };
        doIt();
    }
    foreach (Action action in actions)
    {
        action();
    }
    yield return new WaitForEndOfFrame();
}

I’m still a bit confused why you create a multicast delegate with an empty delegate:

    Action action = new Action(delegate {});

and then subscribe another one:

    action += delegate () {
        Debug.Log("I am action number " + innerCopy);
    };

wouldn’t it be way easier this way:

actions.Add(()=>{
    Debug.Log("I am action number " + innerCopy);
});

Since you subscribe an anonymous delegate you can’t unsubscribe it anyway. Adding more functions / delegates is always possible:

    actions[1] += ()=>{Debug.Log("ANOTHER ONE");};

I don’t have enough permission to comment on Bunny’s excellent answer, but I’d like to mention to anyone having this issue that I solved it by putting the creation and assignment of the anonymous delegate inside another (non-coroutine) function. This let’s closures work as expected, at least for me.

So, to summarize:

IEnumerator yourCoroutine(){
    //fun coroutine stuff
    setOnClickBehavior(data);
    //more coroutine stuff
}

void setOnClickBehavior(object[] params){
    //Make your delegate as usual, it will close around the params given
}