闭包

闭包

之前在学习委托事件的时候了解了一下闭包, 没想到后来在游戏制作中就犯了一个闭包的错误.

使用的是 Unity 引擎, 先看一个 Start 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void Start()
{
GameObject gameObject;
characterPackage = characterDataScript.characterData.Package;

for (int index = 0; index < characterPackage.Count; index++)
{
// 生成菜单项, 并设置父物体
gameObject = Instantiate(itemMenuItemPrefab, itemMenuItemParent.transform);

// 修改菜单项的名称
gameObject.name = index.ToString();

// 修改菜单项的图标
gameObject.transform.Find("Icon").Find("Image").GetComponent<Image>().sprite = characterPackage[index].Icon;

// 修改菜单项的文本
gameObject.transform.Find("Name").Find("Text").GetComponent<Text>().text = characterPackage[index].Name;

// 给菜单项的按钮注册事件, [UnityAction: 一个无参的委托, 返回值无所谓]
gameObject.GetComponent<Button>().onClick.AddListener(() => Event_ItemSelected(gameObject));
}
}

执行步骤:

  1. 新建一个菜单项, 用 gameObject 保存.
  2. 修改菜单项各项属性. 其中名称属性就是遍历索引 index 的值, 即第一个菜单项的名称是 0, 第二个是 1, 第十个是 9 等等;
  3. 给菜单项上的按钮注册事件, 事件的参数为 gameObject, 即将菜单项自身作为参数传递到方法中.

那么问题来了, 假设有 4 个菜单项, 那么在事件 Event_ItemSelected 中输出按钮的名称的话, 依次点击菜单项会输出什么呢?

不是很了解闭包的人应该会说出这个答案: 0, 1, 2, 3, 而正确的答案是: 3, 3, 3, 3!

什么是闭包?

在上面的代码段中, gameObject 变量是在 Start 方法中定义的, 那么按理来说只要出了 Start 方法, gameObject 变量就不存在了. 但是我们在按钮的 Event_ItemSelected 事件中却又访问了变量 gameObject, 这种现象就是闭包.

当一个变量脱离了其自身的作用域后, 根据上下文关系继续在某些方法或者类中发挥作用的现象就是闭包. 或者说闭包可以让变量脱离其作用域继续发挥作用.

答案解析

在上面的代码中, 每次循环都会创建一个菜单项, 并将 gameObject 变量作为参数绑定事件, 所以最后的结果就是 菜单项 0, 菜单项 1, 菜单项 2, 菜单项 3 都使用了 gameObject 变量作为参数, 而 gameObject 是一个变量啊, 不是一个常量, 创建菜单项 0 的时候, gameObject 确实是菜单项 0, 但是等到了创建菜单项 3 的时候, gameObject 也就变成菜单项 3 了, 因此调用事件的瞬间, 使用的也是那个瞬间的 gameObject 值, 也就是 3.

纠正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void Start()
{
characterPackage = characterDataScript.characterData.Package;

for (int index = 0; index < characterPackage.Count; index++)
{
// 生成菜单项, 并设置父物体
GameObject gameObject = Instantiate(itemMenuItemPrefab, itemMenuItemParent.transform);

// 修改菜单项的名称
gameObject.name = index.ToString();

// 修改菜单项的图标
gameObject.transform.Find("Icon").Find("Image").GetComponent<Image>().sprite = characterPackage[index].Icon;

// 修改菜单项的文本
gameObject.transform.Find("Name").Find("Text").GetComponent<Text>().text = characterPackage[index].Name;

// 给菜单项的按钮注册事件, [UnityAction: 一个无参的委托, 返回值无所谓]
gameObject.GetComponent<Button>().onClick.AddListener(() => Event_ItemSelected(gameObject));
}
}

使用这段代码就可以纠正那个错误, 两段代码只有一个区别, 就是 gameObject 变量定义的位置不同. 一个在 for 循环外, 一个在 for 循环内.

  1. 在 for 循环外部定义时, 每一个菜单项使用的都是同一个 gameObject;

  2. 在 for 循环内部定义时, 每一次循环都会重新定义一个 gameObject, 每一个菜单项之间使用的都是不同的 gameObject;

closure