在C#中用另一个对象实例替换对象实例

17
在这个问题中,我想要找出这是否可能,并且如何实现。这种技术看起来非常不好,但似乎我正在使用的API(UnityEditor)正在做某些类似的事情,我只是好奇。
如果有多个引用指向同一对象,是否可以将一个新的对象实例化到同一内存槽中,使得所有先前的引用都指向新对象?
我找出了唯一可行的方法,那就是使用非托管的C++。基本上是下面这样的:
// Original prefab
GameObject prefab = x;
prefab.tag = "Untagged";

// A copy of the original prefab
GameObject prefabCopy = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
prefabCopy.tag = "EditorOnly";  // Change from initial value "Untagged"

Debug.Log(prefab.tag);     // "Untagged"   - expected
Debug.Log(prefabCopy.tag); // "EditorOnly" - expected

// Replace contents of prefab file with `prefabCopy`
PrefabUtility.ReplacePrefab(prefabCopy, prefab);

// Destroy the copy
DestroyImmediate(prefabCopy);

Debug.Log(prefab.tag);     // "EditorOnly"   - whoa?

某种程度上说,prefab现在指向了一个不同的对象?

注意:请记住Unity是建立在Mono .NET框架之上的。


PrefabUtility.ReplacePrefab(prefabCopy, prefab); 内部发生了什么? - Davin Tryon
@dtryon PrefabUtility.ReplacePrefab 调用了一个内部方法(根据程序集浏览器)。 - Lea Hayes
4个回答

8

由于对象状态是由字段值定义的,因此您可以将包含字段值的内存从一个对象复制到另一个对象,有效地“替换”它:

public static void Replace<T>(T x, T y)
    where T : class
{
    // replaces 'x' with 'y'
    if(x == null) throw new ArgumentNullException("x");
    if(y == null) throw new ArgumentNullException("y");

    var size = Marshal.SizeOf(typeof(T));
    var ptr = Marshal.AllocHGlobal(size);
    Marshal.StructureToPtr(y, ptr, false);
    Marshal.PtrToStructure(ptr, x);
    Marshal.FreeHGlobal(ptr);
}

请注意,此代码要求类定义有[StructLayout(LayoutKind.Sequential)](或LayoutKind.Explicit)属性。

UnityEngine.Object具有[StructLayout(LayoutKind.Sequential)] - Lea Hayes
Unity重载了所有的等式运算符,这些运算符会做一些不寻常的事情,比如在被销毁后将正数与“null”进行比较。使用“Marshal”,有没有一种方法可以显示指针的字符串表示形式?我想将“prefab”的引用与“PrefabUtility.ReplacePrefab”的返回值进行比较,而不让Unity执行任何黑魔法。 - Lea Hayes
您可以使用 var gch = GCHandle.Alloc(obj, GCHandleType.Pinned); var address = gch.AddrOfPinnedObject(); gch.Free() 获取某些对象的地址,但对于大多数对象类型,这种方法不起作用。我认为这是您能够获得的最接近对象地址的方法。无论如何,我认为除了垃圾回收器之外,没有什么东西会更改您代码中的引用而不是对象实例。 - max
我原本希望通过推断更新后的预制体引用是否与原始引用相同来判断对象是否被重复使用。不幸的是,对于这种对象类型,“ArgumentException: Object contains non-primitive or non-blittable data.” 这种方法并不起作用。谢谢 :-) - Lea Hayes
我接受了这个答案,因为我认为它最有可能是正确的,特别是考虑到 StructLayout 属性已经存在。 - Lea Hayes

8
如果您将对象嵌入到用于访问该对象的另一个对象中,那么您可以这样做。
class ObjectReference<T>
   where T : new()
{
    private T _obj = new T();

    public void CreateNewObject()
    {
        _obj = new T();
    }

    public T Value { get return _obj; }
}

现在,您可以创建多个指向 ObjectReference 类型对象的引用,并仅更改本地对象。"真实" 对象将通过 Value 属性访问。
稍微不同的方法是创建一个包装器,实现与您的 "真实" 对象相同的接口,从而使这个包装透明。
interface ISomeInterface
{
    string PropertyA { get; set }
    void MethodB (int x);
}

class TheRealObject : ISomeInterface
{
    public string PropertyA { get; set }

    public void MethodB (int x)
    {
        Console.WriteLine(x);
    }
}

class Wrapper : ISomeInterface
{
    TheRealObject _obj = new TheRealObject();

    public string PropertyA
    { 
        get { return _obj.PropertyA; }
        set { _obj.PropertyA = value; }
    }

    public void MethodB (int x)
    {
        _obj.MethodB(x);
    }

    public void CreateNewObject()
    {
        _obj = new TheRealObject();
    }
}

现在,包装器可以像“真实”对象一样使用。您还可以在包装器的构造函数中传递“真实”对象的初始实例,并删除_obj的初始化程序。

如果您的类将属性设置为另一个对象(例如,MyProperty = someOtherInstantiatedObject),会发生什么?现在,如果修改了MyProperty,为什么我看不到someOtherInstantiatedObject中的更改?您会如何处理?谢谢。 - toughQuestions
1
@toughQuestions:当MyProperty包含对someOtherInstantiatedObject的引用并且您修改MyProperty本身时,它将指向另一个对象或null,但不会修改someOtherInstantiatedObject。但是如果您修改MyProperty.SomeProperty,这将更改someOtherInstantiatedObject内部的属性。您必须意识到,类是引用类型。此类变量始终包含引用或null。还请参阅我的答案Setting a type reference type to null doesn't affect copied type? - Olivier Jacot-Descombes
谢谢提供链接,非常清晰 :-) 那么,我该如何解决我的问题呢?直接使用someOtherInstantiatedObject作为MyProperty是唯一的方法吗?因为没有真正的“重命名”它的方式。 - toughQuestions
在Stack Overflow上发布一个问题,展示相关代码并详细解释您的问题。从这些简短的评论中,我很难理解您的问题。 - Olivier Jacot-Descombes

4
不,那是不可能的。
要真正改变所有对对象的引用,您必须冻结进程中的所有线程,并访问它们的寄存器集和堆栈。这就是垃圾回收器所做的事情,但常规代码无法实现。
该方法最有可能实现的是将一个对象深度复制到另一个对象上。

1
我认为这是最有可能的情况。虽然我相信 UnityEngine.Object 不是线程安全的。 - Lea Hayes
1
他们是错误的,他是正确的。事实上,我相信文档明确说明它正在进行复制,而不是传递引用。 - Jerdak

1

如果您想引用自定义类,我认为您可以让所有引用指向一个虚拟引用...

  1. 创建您的类(A)
  2. 创建您的类接口(IA)
  3. 基于您的接口创建一个包装类,该类仅将所有调用传递给包含的对象(AC)

我添加了一个赋值运算符,所以我现在有所有的A对象作为AC。

class AC:IA  
{  
    IA ref;  
    AC(IA ref)  
    {  
        this.ref = ref;  
    }  

    public void ChangeReference(IA newRef) { ref = newRef;}  
    public static operator = (IA assignedObj)  
    {  
        return (assignedObject is AC) ? assignedObject : new AC(assignedObj);
    }

    // implementation of all methods in A
    public override string ToString() { return ref.ToString(); }  
        ...  
}

现在如果你想的话,可以使用ChangeReference方法将所有内容切换到新的引用中。

在C++中,您将使用Reference to Reference。

祝你好运!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接