Unity自定义Dependency Injection字段注入后为空问题排查求助
尝试为Unity编写自定义依赖注入框架时遇到字段注入后NullReferenceException的问题。Player类负责生成Enemy实例,通过反射从Container中获取实例注入Enemy的_player字段。调试发现InitEnemy方法中_player已正确赋值,但Start和Update方法中该字段变为null,引发空引用异常。
调试输出:
Init Enemy player (Player) Start Enemy Null NullReferenceException Null NullReferenceException...
相关代码:
Enemy类代码
public class Enemy : MonoBehaviour { [InjectField] private Player _player; public void InitEnemy() { Debug.Log("Init Enemy " + _player); } private void Start() { Debug.Log("Start Enemy " + _player); } private void Update() { Debug.Log(_player); _player.TakeDamage(10); } }
Player类代码
public class Player : MonoBehaviour { [SerializeField] private Enemy _enemyPrefab; private void Awake() { Container.BindSingleton(this); } public void TakeDamage(int damg) { Debug.Log("player dmg! " + damg); } [Button] public void Spawn() { Enemy spawnedEnemy = InstantiateAndBind(_enemyPrefab); spawnedEnemy.InitEnemy(); } public T InstantiateAndBind<T>(T prefab) where T : MonoBehaviour { T scriptToInject = Instantiate(prefab); Type scriptType = prefab.GetType(); var fieldsToInject = new List<FieldInfo>(); FieldInfo[] fields = scriptType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { if (Attribute.IsDefined(field, typeof(InjectField))) { fieldsToInject.Add(field); } } foreach (FieldInfo bindField in fieldsToInject) { bindField.SetValue(scriptToInject, Container.Get(bindField.FieldType)); } return T; } }
Container类代码
public static class Container { private static readonly Dictionary<Type, object> _bindClasses = new(128); public static void BindSingleton<T>(T instance) where T : class { if (instance == null) { throw new ArgumentNullException(); } if (_bindClasses.ContainsKey(typeof(T))) { throw new ArgumentException(); } _bindClasses.Add(typeof(T), instance); } public static T Get<T>() where T : class { if (_bindClasses.ContainsKey(typeof(T))) { return (T)_bindClasses[typeof(T)]; } else { throw new ArgumentException(); } } public static object Get(Type type) { if (_bindClasses.ContainsKey(type)) { return _bindClasses[type]; } else { throw new ArgumentException(); } } }
疑问
- 该问题的原因是什么?
- 通过反射设置字段值会被垃圾回收器(GC)移除引用吗?
1. NullReferenceException的根本原因
你写的InstantiateAndBind<T>方法存在致命语法错误:
return T;
这里错误地返回了类型参数T,而非实例化并完成注入的scriptToInject对象。这会导致Spawn方法中接收的spawnedEnemy变量为null——但调试日志里Init Enemy能正常打印,说明你实际调用InitEnemy的是scriptToInject这个正确实例,而后续运行Start、Update的是未被注入的Enemy实例(或spawnedEnemy为null,但Unity生命周期方法仍在正确实例上执行),最终出现字段值为空的矛盾现象。
修正方法:将返回语句改为:
return scriptToInject;
除此之外,还有一个潜在问题需要注意:如果你的Enemy存在子类,当前通过prefab.GetType().GetFields()获取字段的方式只会读取当前类型的字段,不会遍历父类。若[InjectField]标记的字段在父类中,需要递归遍历所有父类来收集字段:
Type currentType = scriptType; while (currentType != null && currentType != typeof(MonoBehaviour)) { FieldInfo[] fields = currentType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { if (Attribute.IsDefined(field, typeof(InjectField))) { fieldsToInject.Add(field); } } currentType = currentType.BaseType; }
2. 反射设置字段值会被GC移除引用吗?
不会。GC的回收逻辑是:当一个对象没有任何有效引用时,才会被标记为可回收。通过反射设置的字段引用和普通代码赋值的引用没有区别:
- 只要你的
Player实例在Container的单例字典中存在引用,它就不会被GC回收; - 即使
Player对象被手动销毁(比如调用Destroy(player)),Enemy的_player字段依然会持有该对象的引用,此时访问会抛出MissingReferenceException,而非字段变为null。
字段变为null的唯一可能是被代码主动赋值为null,或者持有该字段的对象被GC回收(但你的Enemy对象在运行Start、Update,显然未被回收)。
内容的提问来源于stack exchange,提问作者UniDev

