0

我目前正在使用 Unity3D 引擎使用 C# 并遇到以下问题:

我创建了一个类,它有两个对它必须访问的另一个类的实例的私有引用。一旦我创建了类的多个实例并设置了引用,我发现所有实例都使用相同的变量。我在销毁一个实例时意识到了这一点,并且在此之前将两个持有引用的变量设置为空。这样做之后,所有其他实例都立即抛出 NullReferenceExceptions,因为它们仍在尝试访问引用。引用的对象很好,其他脚本仍然可以访问它们。

这是一些说明结构的伪代码:

public class Character
{
    // Character data
}

public class StatusEffect
{
    private Character target;
    private Character originator;

    public void Init(Character _Target, Character _Originator)
    {
        target = _Target;
        originator = _Originator;
    }

    public void Destroy()
    {
        target = null;
        originator = null;
    }
}

在程序中它会被这样调用:

StatusEffect effect = new StatusEffect();
effect.Init(player1, player2);

// Time goes by

effect.Destroy();

在调用 Destroy() 之后,每个 StatusEffect 的两个引用都将为空。

这不仅是销毁 StatusEffects 时的问题,而且在创建新的时也是如此。一旦我从一个新实例中触摸引用,所有 StatusEffects 将引用由新 StatusEffect 指定的两个字符。

我不明白为什么或如何解决这个问题。有人可以在这件事上启发我吗?

干杯,瓦尔塔罗斯

编辑:

这是所要求的真实代码:我有一个包含多个 StatusEffects 的容器类。一旦启动,它就会初始化所有这些。

public class CElementTag
{
    // ..Other data..

    public float f_Duration; // Set in the editor

    private CGladiator gl_target;
    private CGladiator gl_originator;
    private float f_currentDuration;

    public CStatusEffect[] ar_statusEffects;

    // Starts the effect of the element tag
    public void StartEffect(CGladiator _Originator, CGladiator _Target)
    {
        gl_originator = _Originator;
        gl_target = _Target;
        f_currentDuration = f_Duration;

        for(int i = 0; i < ar_statusEffects.Length; i++)
            ar_statusEffects[i].Initialize(gl_originator, gl_target);
    }

    // Ends the effect of the element tag
    public void EndEffect()
    {
        for(int i = 0; i < ar_statusEffects.Length; i++)
        {
            if(ar_statusEffects[i] != null)
                ar_statusEffects[i].Destroy();
        }
    }

    // Called every update, returns true if the tag can be destroyed
    public bool ActivateEffect()
    {
        f_currentDuration -= Time.deltaTime;

        if(f_currentDuration <= 0.0f)
        {
            EndEffect();
            return true;
        }

        for(int i = 0; i < ar_statusEffects.Length; i++)
        {
            if(ar_statusEffects[i] != null && ar_statusEffects[i].Update())
                RemoveStatusEffect(i);
        }

        return false;
    }

    // Removes expired status effects
    private void RemoveStatusEffect(int _Index)
    {
        // Call destroy method
        ar_statusEffects[_Index].Destroy();

        // Remove effect from array
        for(int i = _Index; i < ar_statusEffects.Length - 1; i++)
            ar_statusEffects[i] = ar_statusEffects[i+1];

        ar_statusEffects[ar_statusEffects.Length - 1] = null;
}
}

实际的 StatusEffect 类包含两个引用以及它需要工作的一些其他数据。它有虚方法,因为有一些类继承自它。

public class CStatusEffect
{
    // ..Necessary data..

    // References
    protected CGladiator gl_target;
    protected CGladiator gl_originator;

    virtual public void Initialize(CGladiator _Target, CGladiator _Originator)
    {
        gl_target = _Target;
        gl_originator = _Originator;

        // ..Initialize other necessary stuff..
    }

    virtual public void Destroy()
    {
        gl_target = null;
        gl_originator = null;

        // ..Tidy up other data..
    }

    virtual public bool Update()
    {
        // ..Modifying data of gl_target and gl_originator..
        // Returns true as soon as the effect is supposed to end.
    }
}

这应该是关于这个问题的所有相关代码。

编辑2

@KeithPayne我在编辑器中定义了一个静态元素标签数组并保存到xml。在程序开始时,静态数组正在加载 xml 并存储所有元素标签。在创建要使用的新元素标记时,我使用此构造函数:

// Receives a static tag as parameter
public CElementTag(CElementTag _Tag)
{
    i_ID = _Tag.i_ID;
    str_Name = _Tag.str_Name;
    enum_Type = _Tag.enum_Type;
    f_Duration = _Tag.f_Duration;

    ar_statusEffects = new CStatusEffect[_Tag.ar_statusEffects.Length];
    Array.Copy(_Tag.ar_statusEffects, ar_statusEffects, _Tag.ar_statusEffects.Length);
}

我是否必须使用不同的方法将数组复制到新标签?我认为 Array.Copy 会对源数组进行深层复制并将其存储在目标数组中。如果它实际上是在做一个浅拷贝,我明白问题出在哪里了。

4

4 回答 4

1

Array.Copy 方法 (Array, Array, Int32)

如果 sourceArray 和 destinationArray 都是引用类型的数组或者都是 Object 类型的数组,则执行浅拷贝。数组的浅拷贝是一个新数组,其中包含对与原始数组相同元素的引用。不会复制元素本身或元素引用的任何内容。相反,Array 的深层副本复制元素以及元素直接或间接引用的所有内容。

考虑这个StatusEffect类的流利版本及其下面的用法:

public class StatusEffect
{
    public Character Target { get; private set; }
    public Character Originator { get; private set; }

    public StatusEffect Init(Character target, Character originator)
    {
        Target = target.Clone()
        Originator = originator.Clone();
        return this;
    }
//...
}    

public CElementTag(CElementTag _Tag)
    {
        i_ID = _Tag.i_ID;
        str_Name = _Tag.str_Name;
        enum_Type = _Tag.enum_Type;
        f_Duration = _Tag.f_Duration;

        ar_statusEffects = _Tag.ar_statusEffects.Select(eff => 
            new StatusEffect().Init(eff.Target, eff.Originator)).ToArray();

        // ar_statusEffects = new CStatusEffect[_Tag.ar_statusEffects.Length];
        // Array.Copy(_Tag.ar_statusEffects, ar_statusEffects, _Tag.ar_statusEffects.Length);


    }
于 2013-09-27T12:42:08.847 回答
1

因为您通过您的Init()方法传递对对象的引用,所以您实际上并没有“复制”对象,只是维护对内存中相同底层对象的引用。

如果您有多个players对相同底层对象的相同引用,则玩家 1 所做的更改将影响玩家 2 正在使用的对象。

说了这么多,你实际上并没有在你的Destory方法中处理对象。只需将本地实例引用设置为 Null,这不会影响 StatusEffects 的任何其他实例。你确定其他东西没有处理对象,或者你没有正确地初始化你的其他实例。

如果您确实想要获取传入对象的完整副本,请查看 ICloneable 接口。看起来您想将对象的副本传递给每个 Player。

public class Character : ICloneable
{
    // Character data

    //Implement Clone Method
}

public class StatusEffect
{
    private Character target;
    private Character originator;

    public void Init(Character _Target, Character _Originator)
    {
        target = _Target.Clone()
        originator = _Originator.Clone();
    }
于 2013-09-26T13:29:55.523 回答
0

感谢 Keith Payne,我找到了问题所在。我正在创建 CElementTag 的深层副本,但不是我的 ar_statusEffects 数组。我错误地认为 Array.Copy 正在创建数组的深层副本,而实际上并非如此。

我为我的 CStatusEffect 实现了 IClonable 接口,并使用 Clone() 方法为静态数组的每个成员创建了一个真正的深层副本,并将其添加到新的 tags ar_statusEffects 数组中。这样我就有了单独的效果实例,而不是引用相同的静态效果。

感谢大家,尤其是 Keith Payne,感谢他们的帮助和支持!

于 2013-09-27T06:43:59.040 回答
0

这些字段在其他实例之间不共享(静态)。所以调用不会影响其他实例target = null;Destroy()

StatusEffect effect1 = new StatusEffect();
effect1.Init(player1, player2);

StatusEffect effect2 = new StatusEffect();
effect2.Init(player1, player2);

// Time goes by

effect2.Destroy();

// Some more time goes by

// accessing effect1.target won't give a `NullReferenceException` here unless player1 was null before passed to the init.

effect1.Destroy();

我认为您确实忘记了Init(..)其他实例。每次创建 的实例时StatusEffect,都需要调用Init(...).


更新:

此行将清除对效果的引用,但您永远不会重新创建它:

ar_statusEffects[ar_statusEffects.Length - 1] = null;

所以下次你调用ar_statusEffects[x].Update() or Initialize() etc它时会抛出一个 NullReferenceException

如果你想clear out在你的数组中产生效果,你可以在效果中创建一个Enable布尔值,这样你只需要设置/重置它。

    for(int i = 0; i < ar_statusEffects.Length; i++)
        if(ar_statusEffects[i].IsEnabled)
            ar_statusEffects[i].Update();

你为什么不使用列表来代替?只要您不必在其中洗牌,数组就会更快。(如循环缓冲区等)

于 2013-09-26T13:28:51.377 回答