Unity / C# 闭包详解 —— 按钮回调、协程、事件中的坑与修复
在 Unity 开发中,闭包(Closure)经常出现在 按钮回调、协程 和 事件订阅 等场景。
如果不了解闭包机制,很容易踩坑:点击按钮总是输出最后一个值、协程结果不对、事件回调全部相同……
本文将通过详细示例,带你全面理解闭包的本质,以及在 Unity / C# 中的常见问题与解决方法。
📑 目录
-
什么是闭包
-
常见闭包场景
-
按钮回调中的闭包
-
协程中的闭包
-
事件订阅中的闭包
-
-
Unity 实战推荐写法
-
闭包使用注意事项
-
总结
1. 什么是闭包
闭包(Closure) = 函数 + 函数定义时的外部变量引用。
在 C# 中,lambda 和匿名函数就是典型的闭包。
特点:
-
可以访问外层作用域的变量;
-
捕获的是“变量引用”,不是“值快照”。
2. 常见闭包场景
2.1 按钮回调中的闭包
错误写法:
for (int i = 0; i < buttons.Count; i++)
{// ⚠️ 闭包捕获了 i 的引用buttons[i].onClick.AddListener(() => OnButtonClick(i));
}void OnButtonClick(int index)
{Debug.Log("点击按钮: " + index);
}
👉 点击任何按钮,都会打印最后一个 i 的值。
正确写法:
for (int i = 0; i < buttons.Count; i++)
{int index = i; // ✅ 局部变量副本buttons[i].onClick.AddListener(() => OnButtonClick(index));
}
👉 每个按钮绑定的回调都有自己的 index 值,点击第 0 个按钮就输出 0。
2.2 协程中的闭包
错误写法:
IEnumerator Start()
{for (int i = 0; i < 3; i++){// ⚠️ 捕获 i 的引用,循环结束时 i=3StartCoroutine(DoTask(() => Debug.Log(i)));}yield return null;
}IEnumerator DoTask(System.Action action)
{yield return new WaitForSeconds(1);action();
}
👉 输出结果:三次都是 3。
正确写法:
IEnumerator Start()
{for (int i = 0; i < 3; i++){int index = i; // ✅ 局部变量副本StartCoroutine(DoTask(() => Debug.Log(index)));}yield return null;
}
👉 输出结果:0, 1, 2。
2.3 事件订阅中的闭包
错误写法:
public class EventClosureExample : MonoBehaviour
{public delegate void TestEvent();public static event TestEvent OnTest;private void Start(){for (int i = 0; i < 3; i++){// ⚠️ 捕获 i 的引用OnTest += () => Debug.Log(i);}OnTest?.Invoke();// 输出:3, 3, 3}
}
正确写法:
for (int i = 0; i < 3; i++)
{int index = i; // ✅ 局部变量副本OnTest += () => Debug.Log(index);
}OnTest?.Invoke();
// 输出:0, 1, 2
3. Unity 实战推荐写法
3.1 UI 按钮绑定(防止重复绑定)
for (int i = 0; i < buttons.Count; i++)
{int index = i; // ✅ 副本buttons[i].onClick.RemoveAllListeners(); // 避免重复绑定buttons[i].onClick.AddListener(() => HandleClick(index));
}void HandleClick(int index)
{Debug.Log("点击按钮: " + index);
}
3.2 协程任务写法
IEnumerator RunTasks()
{for (int i = 0; i < 3; i++){int index = i; // ✅ 副本yield return StartCoroutine(Task(index));}
}IEnumerator Task(int id)
{yield return new WaitForSeconds(1);Debug.Log("任务完成:" + id);
}
3.3 事件订阅写法
void RegisterEvents()
{for (int i = 0; i < 3; i++){int index = i; // ✅ 副本EventManager.OnSomething += () => Debug.Log("事件: " + index);}
}
4. 闭包使用注意事项
-
循环里的闭包 → 必须创建局部副本 (
int index = i;
)。 -
协程/异步场景 → 同样要用局部副本,否则会全部捕获最后一个值。
-
事件订阅 → 静态事件 + 闭包可能导致内存泄漏(要及时取消订阅)。
-
推荐习惯:写闭包时,先复制变量,再用到 lambda 里。
5. 总结
-
闭包是 Unity / C# 开发中常见的机制,匿名函数和 lambda 都会捕获外部变量。
-
最大的坑:循环捕获变量,导致输出总是最后一个值。
-
修复方法:在循环内部新建局部变量副本。
-
场景:按钮回调、协程、事件订阅 → 都要特别注意闭包问题。
💡 建议:
-
写闭包前 → 先问自己“是不是在循环里?是不是要捕获变量?”。
-
如果是 → 就用
int index = i;
这样的方式创建局部副本,永远不会出错。
📝 闭包练习题
下面的代码里,存在闭包相关的问题。
请读者思考:点击按钮时,输出结果会是什么?为什么?
练习代码:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;public class ClosurePractice : MonoBehaviour
{public List<Button> buttons;private void Start(){for (int i = 0; i < buttons.Count; i++){// ❓ 闭包会捕获什么?点击不同按钮时会输出什么?buttons[i].onClick.AddListener(() => Debug.Log("按钮索引:" + i));}}
}
👉 思考题:
-
点击第 0 个按钮,会输出几?
-
点击最后一个按钮,会输出几?
-
为什么结果可能和预期不同?
✅ 答案解析
1. 实际运行结果
-
无论点击哪个按钮,都会输出 buttons.Count 的值(即循环结束时
i
的最终值)。
2. 原因分析
-
因为闭包捕获的是 变量引用,而不是当时的“值快照”。
-
当循环结束后,
i
已经变成了buttons.Count
,所有闭包里访问到的i
都是这个最终值。
3. 正确写法(修复版)
for (int i = 0; i < buttons.Count; i++)
{int index = i; // ✅ 使用局部变量副本buttons[i].onClick.AddListener(() => Debug.Log("按钮索引:" + index));
}
👉 修复后:
-
点击第 0 个按钮 → 输出 0;
-
点击第 1 个按钮 → 输出 1;
-
点击第 2 个按钮 → 输出 2;
…完全符合预期。
🎯 思考延伸
-
如果把上面的代码改成 协程,会不会遇到相同的问题?
-
在 事件订阅 中,为什么闭包也可能导致回调结果不对,甚至引发 内存泄漏?
-
尝试修改练习代码,把
for
改成foreach
,看看结果是否一样?为什么?
📌 小结:
-
练习中展示了闭包最典型的坑:循环变量被捕获引用。
-
解决方案永远是:使用局部变量副本(
int index = i;
)。