Unity中C# Dictionary移除元素失败的异常问题求助
我来帮你捋捋这个诡异的问题——明明初始字典里有Key,移除失败就算了,之后还直接变空,结合你说换string键测试正常的情况,大概率是Unity生命周期调用时机冲突或者管理类实例不唯一导致的,咱们一步步拆解排查:
核心排查点
1. 确认你操作的是不是同一个字典实例
这是最常见的坑:如果你的WaveControler在场景中有多个实例,或者在敌人销毁前已经被重新创建/销毁,那么你调用EnemyDied时访问的remainingMobs可能已经是一个新的空字典了,而非最初存储敌人的那个。
验证方法:
在WaveControler的Awake和OnDestroy方法中加日志,同时在EnemyDied开头打印当前字典的哈希码(用来区分实例):
private void Awake() { print($"WaveController instance created, dict hash: {remainingMobs.GetHashCode()}"); } private void OnDestroy() { print($"WaveController instance destroyed, dict hash: {remainingMobs.GetHashCode()}"); } public void EnemyDied(int someID) { print($"EnemyDied called, current dict hash: {remainingMobs.GetHashCode()}, count: {remainingMobs.Count}"); // 原有的移除逻辑... }
如果敌人销毁时打印的字典哈希码和最初添加敌人时的不一致,说明你操作的是不同的字典实例。
2. 检查MobID是否被意外修改
虽然你说MobID是自增int,但如果敌人对象在生命周期中,MobID被其他代码意外修改(比如赋值错误、序列化问题),那么调用EnemyDied时传入的ID就不是当初添加到字典的Key了。
验证方法:
把MobID改成只读属性,避免被意外修改,同时在添加和移除时打印ID值:
// EnemyControler中 public int MobID { get; private set; } // 生成敌人时赋值 public void Init(int mobId) { MobID = mobId; print($"Enemy initialized with ID: {MobID}"); } // WaveControler添加时 remainingMobs.Add(newEnemy.MobID, newEnemy); print($"Added enemy ID: {newEnemy.MobID}, dict count: {remainingMobs.Count}");
3. OnDestroy的调用时机陷阱
Unity中OnDestroy的调用顺序是不确定的:如果WaveControler和敌人对象同时被销毁(比如场景切换、批量销毁),WaveControler可能先被销毁,此时它的remainingMobs虽然是引用类型,但后续敌人调用EnemyDied时,访问的是已经被标记为销毁的WaveControler实例,可能出现不可预期的行为(比如字典被隐式清空)。
解决方案
方案1:确保WaveControler是唯一单例
用单例模式保证全局只有一个WaveControler实例,避免操作错误的字典:
public class WaveControler : MonoBehaviour { public static WaveControler Instance { get; private set; } private Dictionary<int, EnemyControler> remainingMobs = new Dictionary<int, EnemyControler>(); private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; // 如果跨场景保留,加上下面这句 DontDestroyOnLoad(gameObject); } // 其他代码... }
然后敌人调用时直接用单例:
// EnemyControler的OnDestroy中 WaveControler.Instance.EnemyDied(MobID);
方案2:提前调用移除逻辑,避免依赖OnDestroy
不要等到OnDestroy才调用移除,而是在敌人死亡的逻辑中主动调用,再销毁对象:
// EnemyControler的死亡处理方法 public void OnEnemyKilled() { WaveControler.Instance.EnemyDied(MobID); Destroy(gameObject); }
这样能确保在对象销毁前,字典的移除操作已经完成,不会受生命周期顺序影响。
方案3:加线程安全锁(可选)
如果你的项目中有非主线程操作字典的情况(比如后台加载敌人),需要加锁避免并发修改:
private readonly object _dictLock = new object(); // 添加元素时 lock(_dictLock) { remainingMobs.Add(newEnemy.MobID, newEnemy); } // 移除元素时 public void EnemyDied(int someID) { lock(_dictLock) { if(remainingMobs.Remove(someID)) { print("removed "+someID); } else { print("Failed to remove " + someID); DisplayRemainingMobs(); } } }
总结
你遇到的情况最可能是操作了错误的字典实例或者OnDestroy时机晚于WaveControler销毁,先通过日志验证字典实例的一致性,再用单例+提前调用移除的方式解决问题。
内容的提问来源于stack exchange,提问作者Weird




