Unity / C# 开发常见问题总结(闭包、协程、事件、GC 等易踩坑)
📑 目录
前言
闭包问题(Lambda 捕获陷阱)
协程与生命周期
事件订阅与取消订阅
异步任务(async/await, UniTask)
对象池与引用残留
内存与 GC(垃圾回收)
静态变量与单例陷阱
多线程与 Unity API 限制
序列化与 Inspector 限制
Update 滥用问题
结语
1. 前言
在 Unity / C# 开发中,很多问题不是语法错误,而是 逻辑陷阱。
这些陷阱往往隐藏在闭包、协程、事件订阅、异步任务等常见功能中。
本文将通过 具体示例 + 修复方案,带你逐个击破。
2. 闭包问题(Lambda 捕获陷阱)
闭包是 C# 的重要特性,但在 Unity 中很容易出错。
❌ 错误示例
for (int i = 0; i < 3; i++)
{button.onClick.AddListener(() =>{Debug.Log("点击按钮:" + i);});
}
// 点击三个按钮都输出 3
✅ 修复方法
for (int i = 0; i < 3; i++)
{int index = i; // 捕获局部变量button.onClick.AddListener(() =>{Debug.Log("点击按钮:" + index);});
}
📌 总结:循环中的 Lambda 捕获变量时,务必用局部变量保存一份。
3. 协程与生命周期
协程不会随对象销毁自动停止,可能访问到已销毁对象。
❌ 错误示例
private IEnumerator DoWork()
{yield return new WaitForSeconds(3f);Debug.Log(gameObject.name); // 对象可能已销毁
}
✅ 修复方法
private IEnumerator DoWork()
{yield return new WaitForSeconds(3f);if (this != null) // 检查对象是否还存在Debug.Log(gameObject.name);
}private void OnDestroy()
{StopAllCoroutines(); // 手动清理
}
📌 总结:协程要么手动停止,要么检查对象状态。
4. 事件订阅与取消订阅
忘记取消订阅会导致 内存泄漏。
❌ 错误示例
private void OnEnable()
{Player.OnDie += HandleDie;
}
如果没写
OnDisable
,即使对象销毁,委托里仍然保存着它。
✅ 修复方法
private void OnEnable()
{Player.OnDie += HandleDie;
}private void OnDisable()
{Player.OnDie -= HandleDie; // 保持对称
}
📌 总结:订阅事件时,必须成对取消。
5. 异步任务(async/await, UniTask)
异步执行时,可能在 await
后对象已被销毁。
❌ 错误示例
private async void LoadData()
{await Task.Delay(2000);Debug.Log(gameObject.name); // 可能对象已经没了
}
✅ 修复方法(推荐 UniTask)
private async UniTaskVoid LoadData(CancellationToken token)
{await UniTask.Delay(2000, cancellationToken: token);if (this == null) return; // 检查对象Debug.Log(gameObject.name);
}
📌 总结:异步任务要考虑对象销毁,避免空引用。
6. 对象池与引用残留
对象池回收后未重置,可能出现旧数据。
❌ 错误示例
enemy.SetHP(0);
objectPool.Release(enemy);
// 下次取出时 HP 依然是 0
✅ 修复方法
public void Reset()
{hp = maxHp;transform.position = Vector3.zero;
}objectPool.Release(enemy);
enemy.Reset();
📌 总结:回收前要清理数据,避免脏状态。
7. 内存与 GC(垃圾回收)
频繁分配临时对象会引发 GC 卡顿。
❌ 错误示例
void Update()
{string log = "位置:" + transform.position; // 每帧 GCDebug.Log(log);
}
✅ 修复方法
private StringBuilder sb = new StringBuilder();void Update()
{sb.Clear();sb.Append("位置:").Append(transform.position);Debug.Log(sb.ToString());
}
📌 总结:避免在 Update 里 new
对象。
8. 静态变量与单例陷阱
静态变量会跨场景存在,容易残留旧状态。
✅ 示例(安全单例)
public class GameManager : MonoBehaviour
{public static GameManager Instance { get; private set; }private void Awake(){if (Instance != null && Instance != this){Destroy(gameObject);return;}Instance = this;DontDestroyOnLoad(gameObject);}
}
📌 总结:注意静态变量清理,避免跨场景数据残留。
9. 多线程与 Unity API 限制
Unity 大部分 API 只能在主线程调用。
❌ 错误示例
Task.Run(() =>
{transform.position = Vector3.zero; // 报错:只能在主线程调用
});
✅ 修复方法
Task.Run(() =>
{var result = HeavyCalculation();UnityMainThreadDispatcher.Instance.Enqueue(() =>{transform.position = result;});
});
📌 总结:子线程只做计算,涉及 Unity API 必须切回主线程。
10. 序列化与 Inspector 限制
Unity 序列化不支持 Dictionary
。
✅ 替代方案
[System.Serializable]
public class StringIntPair
{public string key;public int value;
}public List<StringIntPair> dictLike;
📌 总结:Inspector 里用 List
替代 Dictionary
。
11. Update 滥用问题
不要在 Update
里写过多逻辑。
❌ 错误示例
void Update()
{if (Input.GetKeyDown(KeyCode.Space))Fire();
}
✅ 修复方法
void Start()
{myButton.onClick.AddListener(Fire); // 事件驱动
}
📌 总结:能用事件,就别把逻辑堆在 Update
里。
12. 结语
本文总结了 Unity / C# 开发中最常见的坑点:
闭包变量捕获
协程未停止
事件忘记取消订阅
异步对象销毁
对象池状态残留
GC 卡顿
静态变量跨场景
多线程 Unity API 限制
序列化不支持 Dictionary
Update 滥用
一句话总结:
Unity 开发不仅仅是写功能,更重要的是 理解生命周期和内存管理,否则很容易留下隐患。