- Home /
Passing a temporary variable to add listener
Hi I am trying to pass a method to an add listener when the button is clicked. First I instantiated the object and assign a parent for it. Then I add a listener to the object. The problem is that the only way I know how to pass a method with parameter in the add listener event of unity is through lambda expression. So when I click the button it always passes the last item I iterated to.
Here is a snippet of my code:
foreach(CardData cardItem in p_cards)
{
if(cardItem.Type == CardType.Normal)
{
if(count == 0)
{
cardItem.Name = "CARD ONE";
}
else{
cardItem.Name = "CARD TWO";
}
GameObject m_card = Instantiate(m_regularDetailCard) as GameObject;
m_card.transform.SetParent(m_detailCardContainer.transform);
m_card.GetComponent<RectTransform>().SetDefaultScale();
m_card.GetComponent<Button>().onClick.AddListener(() => {InitializeCardScreen(cardItem);});
}
count++;
}
So what happens is that the parameter I passed in my InitializeCardScreen method will always be "CARD TWO" since it is the last object I instantiated and also since lambda only assigns the value of cardItem when the event onClick is triggered.
Is there any work around for this problem aside for creating ids for each instantiated item?
Answer by Mmmpies · Feb 24, 2015 at 11:42 AM
It's a "feature" of Lambdas, makes no sense to me but I'm told it's expected behavior, although I'm also told it should change in 5 but that's just anecdotal as I don't have 5.
You need to make a copy of anything you want to access for the lambda like this:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class LambdaMenus : MonoBehaviour {
public GameObject prefabButton;
public RectTransform ParentPanel;
// Use this for initialization
void Start () {
for(int i = 0; i < 5; i++)
{
GameObject goButton = (GameObject)Instantiate(prefabButton);
goButton.transform.SetParent(ParentPanel, false);
goButton.transform.localScale = new Vector3(1, 1, 1);
Button tempButton = goButton.GetComponent<Button>();
int tempInt = i;
tempButton.GetComponentInChildren<Text>().text = "Button Numero " + i.ToString ();
tempButton.onClick.AddListener(() => ButtonClicked(tempInt));
}
}
//void SetButtonValues(Bu
void ButtonClicked(int buttonNo)
{
Debug.Log ("Button clicked = " + buttonNo);
}
}
You should be able to get the button number back on click so it can identify which button is which.
I ran into this issue myself and your answer was a godsend on getting it working, thank you so much.
I did some testing and found that you don't need the tempButton to make it stick, just the tempInt.
It is worth mentioning that if you do this inside an IEnumerator, it does not work the same way.
Inside the IEnumerator the 'tempInt' will be the same for every tempButton, which is the last value that the 'tempInt' gets from the For loop.
I was running into this problem a few days ago and I couldn't figure out what it was, then I put the AddListener outside of the IEnumerator and it magically worked. Like this:
IEnumerator ExampleButtonLoop()
{
for (int i = 0; i < tempButtons.Length; i++)
{
AddListenerToButton(i);
}
yield break;
}
void AddListenerToButton(int i)
{
tempButtons[i].onClick.AddListener(() => OnClickedButton(i));
}
void OnClickedButton(int i)
{
Debug.Log("Clicked Button: " + i);
}
If anyone could clarify to me why, then please.
$$anonymous$$y guess was that it doesn't allow the Listener to save the copy of it, but rather just a reference. (Also tried having tempInt non-local).
The problem you encountered comes from the fact that a coroutine is not a normal method. The code inside a coroutine gets translated into a statemachine-class. When you execute your coroutine it returns that statemachine which you have to pass to StartCoroutine.
A lambda expression like this:
() => OnClickedButton(i)
Is just another way to write an anonymous method. However since your method body OnClickedButton(i)
uses a variable from a different scope the methods becomes a closure. In short: If an anomymous method / lambda expression uses a local variable that doesn't belong to it's own scope, the compiler creates a hidden closure class where the variable(s) that is(are) accessed are stored. What way the compiler creates a new "scope" that can be shared between the different methods. When you use the local variable the compiler creates an instance of that class and uses the variable inside that instance. That way the closure can also reference the same instance and use the variable.
What most seem to not understand is that a closure does not just copy the value of a variable, but can access the variable itself.
A quick (but kinda strange) example:
void CreateClosures(out Func<int> getter, out Action<int> setter)
{
int var = 42; // local variable
getter = ()=>var;
setter = (v)=>var = v;
}
Func<int> g;
Action<int> s;
CreateClosures(out g, out s);
print(g()); //--> prints "42"
s(123);
print(g()); //--> prints "123"
Here both closures ("g" and "s") close around the local variable "var" which only exists inside the CreateClosures method. That will turn that local variable into a member variable of the auto generated closure class. When you execute CreateClosures it will create an instance of that class and both closures will use the same closure object and thus use the same variable even when the CreateClosures method has already returned.
A normal for-loop like this:
for(int i = 0; i < 5; i++)
{
// loop body
}
is just a shorthand for:
{
int i = 0;
while (i < 5)
{
// loop body
i++;
}
}
Note: the outer "brackets" limit the scope of "i" so it only exists inside the for loop. Of course when you create a closure inside the loop body, each closure you create will close over the same variable "i" and thus when the closure is executed later it will only "see" the last value that "i" had.
Wow! That's a bunch of information! Thank you for clarifying and linking me to the extra article about Closures, I never knew all of this even existed.
Cheers!
Another "small" addon:
The foreach loop does something similar. The loop variable is only declared once outside the loop and reused each iteration. That's why all closures use the same variable.
In the "C# 5" specs only the foreach loop behaviour has changed. The for loop sill works the same way. Though Unity uses an old $$anonymous$$ono version and not the latest .NET framework so the compiler that comes with Unity will still use the old behaviour.
In short why the "temp-local-variable" trick doesn't work in coroutines:
Imagine this IEnumerator
IEnumerator Test()
{
Action[] actions = new Action[5];
for(int i = 0; i <actions.Length; i++)
{
int temp = i;
actions[i] = ()=>print("action: " + temp);
}
for(int i=0; i < actions.Length; i++)
{
actions[i](); // --> always prints "5"
yield return null;
}
}
This method get converted into a class like this:
private class Test_Enumerator : IEnumerator
{
int state = 0;
Action[] actions;
int i1;
int i2
int temp;
object current;
public bool $$anonymous$$oveNext()
{
if (state == 0)
{
actions = new Action[5];
i1 = 0;
while (i1 <actions.Length;)
{
temp = i1;
actions[i1] = ()=>print("action: " + temp);
i1++;
}
i2 = 0;
if (i2 < actions.Length)
{
actions[i]();
current = null; // "yield return null"
state = 1;
return true;
}
return false;
}
else if(state == 1)
{
i2++;
if (i2 < actions.Length)
{
actions[i]();
current = null; // "yield return null;"
return true;
}
return false;
}
}
public object Current
{
get { return current;}
}
}
IEnumerator Test()
{
return new Test_Enumerator();
}
As you can see all local variables are turned into member variables of the IEnumerator class. That's why the closures still closes over the same "temp" variable. Note: This is actually kind of a "bug" in the mono compiler. In my example only "i2" would need to be a member variable. The $$anonymous$$icrosoft C# compiler is more exact here. A loop that doesn't contain a yield can be executed "normally" and can use a true local variable. Though if the loop contains a yield statement the code need to be spilt into states and all variables that need to survive between states need to be a member variable of the IEnumerator.
So the 'temp' is a member variable ins$$anonymous$$d of a true local variable because of the states in the IEnumerator? Which means the 'temp' in the AddListener, once fired, will get the member variable, which by then is already the final value of the iteration and not the value that it was once I added it to the AddListener.
So if I were to go outside of the IEnumerator and use a normal method, it can be a 'true' variable and thus will be in its own class/closure which means there are actually 5 times the int 'temp' ? Wait I think I am confusing myself now...
Your answer
Follow this Question
Related Questions
anonymous functions with JavaScript? 2 Answers
Change size of the new UI rect transform using scripts 0 Answers
scrollrect dose not response 1 Answer
How do I make a touch thumb stick with UGui? 0 Answers
MIP Maps - uGUI 0 Answers