You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    }
}

疑问

  1. 该问题的原因是什么?
  2. 通过反射设置字段值会被垃圾回收器(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 12:10:39