问题

本文主要针对的问题是在Unity中对Button类进行Onclick事件绑定的时候出现的函数参数错误进行分析解决,具体问题如下:

Button[] button = GetComponentsInChildren<Button>();
int buttonCnt = 3;

for (int i = 0; i < buttonCnt; i++)
{      
    Debug.Log("i: " + i);
    button[i].OnClick.AddListener( () => ClickButton(i) );
}

public void ClickButton(int addScore)
{
    Debug.Log("addScore: " + addScore);
    score = score + addScore;
}

这段代码中主要对buttonCnt个按钮绑定Onclick事件,期望用户在点击不同按钮的时候能给score加上不同的分数,但是实际上不管在点击任何按钮以后都会得到如下输出:

i: 0
i: 1
i: 2
addscore: 3

与我们的预期不符,我们期待在按不同按钮的时候能得到按钮的id:i,但是实际上我们不管按哪个按钮都会得到一个奇怪的数字3。如果我们仔细观察,可以注意到3恰好是遍历结束以后i的值,那这其中到底发生了什么呢?

原因

出现上述问题的原因主要与闭包(Closure)的特性有关。闭包的定义如下

闭包是一个绑定到声明它的环境中的函数。因此,该函数可以在其函数体中引用环境中的元素。换句话说,闭包是指有权访问另一个函数作用域中的变量的函数

上一段代码实际上是声明了一个匿名方法,再把这个匿名方法作为Onclick的事件。而匿名方法是一个闭包,并且绑定到它的父方法体和其中的局部变量上。其中我们需要注意的是:匿名方法绑定到变量上,而不是值。换句话说,当“ClickButton”被声明时,“i”的值没有被复制进来。相反,匿名方法将使用对“i”的引用,这样“ClickButton”将始终使用“i”的最新值。因此在给按钮进行事件绑定以后,不管用户对哪个按钮进行点击,都会调用"i"的最新值,也就是3。事实上,即使“i”超出了作用域,对“i”的引用也将持续发挥作用,如以下例子:

delegate void Action();

static Action GetAction()
{
  int i = 0;

  Action a = delegate { Console.WriteLine(i); };

  i = 1;

  return a;
}

static void Main(string[] args)
{
  Action a = GetAction();

  a();
}

尽管在"a"被调用的位置已经不在局部变量"i"的作用域,但是输出仍然是1而不是0

解决方案

在了解了闭包的特性以后要解决这个问题就变得很简单了,只需要保证在给ClickButton传参的时候传入一个新的变量,而不是一个持续在使用的变量就可以了,因此可以将

button[i].OnClick.AddListener(delegate { () => ClickButton(i); });

修改为

int temp = i
button[i].OnClick.AddListener(delegate { () => ClickButton(temp); });

这样每次传入ClickButton的都是一个重新声明的局部变量,可以很好地解决这个问题

参考资料

1.https://stackoverflow.com/questions/40183703/c-sharp-unity-wrong-argument-of-the-function-in-onclick-event 2.https://web.archive.org/web/20150707082707/http://diditwith.net/PermaLink,guid,235646ae-3476-4893-899d-105e4d48c25b.aspx