- Home /
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: http://stackoverflow.com/questions/2226510/closures-in-c-sharp-event-handler-delegates
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.
Answer by Bunny83 · Jul 17, 2013 at 09:10 AM
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 ;)
It's not that easy to spot but if you understand what a coroutine is it makes totally sense ;)
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");};
Tried it again with the BetterList implementation that comes with NGUI and it still works the same...
Is this really your actual code?
Great answer! Thanks for the explanation! I usually use action = () => {}
, I used the multicast delegate here only because the code was mostly copied from some example code. And since the example code was supposed to work flawlessly, I wanted to stick with it for this question.
Thanks for this answer. Googling answers to C# problems is really hard considering Unity breaks it so hard.
Answer by VRtube · Feb 05, 2016 at 08:54 PM
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
}
Your answer
Follow this Question
Related Questions
Generic Triggers and Actions 0 Answers
Lifespan of a Delegate with respect to the Container 1 Answer
Delegate does not contain a definition for 'CreateDelegate' 1 Answer
`System.IO.File' does not contain a definition for `AppendText'? 1 Answer
How do I get rid of "Assembly-CSharp-firstpass" and "Assembly-CSharp-firstpass.dll" from my Assets? 1 Answer