C# 结构体的自动深拷贝

15

我有一个叫做MyStruct的结构体,其中包含一个私有成员变量private bool[] boolArray;和一个方法ChangeBoolValue(int index, bool Value)

我还有一个类MyClass,它有一个字段public MyStruct bools { get; private set; }

当我从一个现有的MyStruct对象创建一个新对象,然后应用ChangeBoolValue()方法时,由于复制的是引用而不是被引用的内容,所以两个对象中的bool数组都会改变。例如:

MyStruct A = new MyStruct();
MyStruct B = A;  //Copy of A made
B.ChangeBoolValue(0,true);
//Now A.BoolArr[0] == B.BoolArr[0] == true
有没有一种强制复制实现更深层次的复制的方法,或者有没有一种实现方式不会有相同的问题?
我之前将MyStruct定义为结构体是因为它是值类型,我不想要引用传递。

2
可能有点晚了,而且我不知道你的设计或要求的具体细节,但一般来说,可变结构体是有害的。https://dev59.com/03NA5IYBdhLWcg3wfNyO#945708 - Chris Sinclair
谢谢,那个答案基本上描述了我遇到的问题 :P - 3Pi
7个回答

13
运行时会对结构体进行快速的内存复制,据我所知,不可能引入或强制使用自己的复制过程。您可以引入自己的 Clone 方法或复制构造函数,但无法强制使用它们。
如果可能的话,最好使您的结构体成为不可变的(或者是不可变类),或者重构以避免这个问题。如果您是 API 的唯一使用者,那么可能只需要额外保持警惕。
Jon Skeet(和其他人)曾经描述过这个问题,尽管有例外,但一般来说:可变结构体是有问题的。 Can structs contain fields of reference types

3

一种简单的制作(深层)副本方法,虽然不是最快的方法(因为它使用反射),但可以使用BinaryFormatter将原始对象序列化到一个MemoryStream中,然后从那个MemoryStream反序列化到一个新的MyStruct中。

    static public T DeepCopy<T>(T obj)
    {
        BinaryFormatter s = new BinaryFormatter();
        using (MemoryStream ms = new MemoryStream())
        {
            s.Serialize(ms, obj);
            ms.Position = 0;
            T t = (T)s.Deserialize(ms);

            return t;
        }
    }

适用于类和结构体。


我不确定这是否真正回答了问题。这要求MyStruct的消费者在传递实例时调用DeepCopy方法。我认为3Pi正在询问如何在运行时复制结构体时自动执行深度复制。 - Chris Sinclair
@ChrisSinclair:由于赋值操作会创建一个别名(在他的代码中,object.ReferenceEquals(A, B) 为 true),因此仅使用赋值运算符将无法实现他所要求的功能。除非使用(扩展)方法,否则我不确定是否有其他方法可以解决这个问题。 - Eric J.
@Chris,是的,这就是我的意思——希望除了结构本身之外不必更改任何内容。 - 3Pi
1
@Eric,结构体是值类型。这是否意味着当您将1分配给另一个时,它会被复制?如果不是,那么结构体作为值类型意味着什么? - 3Pi
@3Pi 我不确定这是可能的。运行时对结构体进行快速内存复制(就我所知),没有方法来实现或覆盖这种行为。@EricJ 是的,你说得很对。但再次强调,拥有扩展方法只是为了方便消费者。如果您可以控制MyStruct的使用,那么应该没问题,只需要注意其使用情况即可。 - Chris Sinclair
显示剩余4条评论

2

作为一种解决方法,我将实现以下内容。

结构体中有两种方法可以修改BoolArray的内容。与其在复制结构体时创建数组,当调用更改函数时,将新创建BoolArray,如下所示:

public void ChangeBoolValue(int index, int value)
{
    bool[] Copy = new bool[4];
    BoolArray.CopyTo(Copy, 0);
    BoolArray = Copy;

    BoolArray[index] = value;
}

尽管这对于涉及到大量更改BoolArray的任何用途都是不好的,但我使用结构体进行大量复制,而几乎没有什么变化。只有在需要更改时才会更改对数组的引用。

为什么不使用持久化不可变集合类型?Eric Lippert在他的博客中有一系列很好的文章。 - phoog
我实际上选择了不可变结构体,正如Chris建议的那样。它需要最少的外部更改和较少的内部更改。这是一个我想到的想法,但会产生糟糕的代码气味。 - 3Pi

1
为了避免奇怪的语义,任何持有可变引用类型字段的结构体必须做以下两件事之一:
  1. 它应该非常清楚地表明,从其角度来看,字段的内容不是“持有”一个对象,而仅仅是标识一个对象。例如,`KeyValuePair<String, Control>` 就是一个完全合理的类型,因为虽然 `Control` 是可变的,但是由这种类型引用的控件的身份是不可变的。
  2. 可变对象必须是由值类型创建的,并且永远不会在其外部公开。此外,在将对该不可变对象的任何更改存储到结构体的任何字段之前,必须执行将要执行的所有更改。
正如其他人指出的那样,允许结构体模拟数组的一种方法是让它持有一个数组,并在修改元素时每次创建一个新的数组副本。当然,这样做会极其缓慢。另一种替代方法是添加一些逻辑以存储最近几个突变请求的索引和值;每当尝试读取数组时,检查该值是否是最近写入的值之一,如果是,则使用结构体中存储的值而不是数组中的值。一旦结构体中的所有“插槽”被填满,就制作数组的副本。如果更新命中许多不同元素,则此方法最多“仅”提供恒定的加速效果,但如果极大多数更新命中少量的元素,则可能很有帮助。

当更新可能具有高度的特殊集中性,但命中太多元素以至于无法完全适应结构时,另一种方法是保留对“主”数组的引用,以及一个“更新”数组和一个整数,指示“更新”数组表示主数组的哪个部分。更新通常需要重新生成“更新”数组,但它可以比主数组小得多;如果“更新”数组变得太大,则可以重新生成主数组,并将“更新”数组所代表的更改合并到其中。

任何这些方法的最大问题是,虽然可以通过工程化的方式设计struct,以呈现一致的值类型语义,同时允许高效的复制,但是一眼看去,结构体的代码很难显而易见(与普通数据结构不同,普通数据结构具有名为Foo的公共字段,使得Foo的行为非常清晰明了)。


1

当传递结构体时,它会被复制,对吗?所以:

public static class StructExts
{
    public static T Clone<T> ( this T val ) where T : struct => val;
}

使用方法:

var clone = new AnyStruct ().Clone ();

当一个结构体作为参数传递时,它会被复制,所以我只返回副本。请注意,如果结构体包含类,则只复制它们的引用而不是实例。了解更多关于浅拷贝和深拷贝的知识。 - Arutyun Enfendzhyan

0
    unsafe struct MyStruct{///add unsafe
        private fixed bool boolArray[100];///set to fixed array
        public void ChangeBoolValue(int index, bool Value) {
            boolArray[index] = Value;
        }
    }

    MyStruct copyMyStruct(MyStruct copy){
        return copy;
    }

项目->属性->构建->允许不安全代码 = 已选中

项目->属性->构建->优化代码 = 未选中

    MyStruct A = new MyStruct();
    MyStruct B = copyMyStruct(A);//Copy of A made
    B.ChangeBoolValue(0,true);//B.BoolArr[0] == true , A.BoolArr[0] == false
   

0

我曾思考过一个与值类型相关的类似问题,并找到了一个“解决方案”。你看,在C#中,你无法像在C++中那样更改默认的复制构造函数,因为它旨在轻量级且无副作用。然而,你可以等到实际访问结构体时再检查是否已复制。

问题在于,与引用类型不同,结构体没有真正的身份;只有按值相等性。但是,它们仍然必须存储在某个内存位置,并且该地址可以用于识别(尽管是暂时的)值类型。GC在这里是一个问题,因为它可以移动对象,从而更改结构体所在的地址,因此你必须能够应对这种情况(例如使结构体的数据私有化)。

实际上,如果是值类型,结构体的地址可以从this引用中获取,因为它是一个简单的ref T。我将获得从我的库的引用中获取地址的方法留给您自己去探索,但是发出自定义CIL也很简单。在这个例子中,我创建了一个类似于按值传递数组的东西。
public struct ByValArray<T>
{
    //Backup field for cloning from.
    T[] array;

    public ByValArray(int size)
    {
        array = new T[size];
        //Updating the instance is really not necessary until we access it.
    }

    private void Update()
    {
        //This should be called from any public method on this struct.
        T[] inst = FindInstance(ref this);
        if(inst != array)
        {
            //A new array was cloned for this address.
            array = inst;
        }
    }

    //I suppose a GCHandle would be better than WeakReference,
    //but this is sufficient for illustration.
    static readonly Dictionary<IntPtr, WeakReference<T[]>> Cache = new Dictionary<IntPtr, WeakReference<T[]>>();

    static T[] FindInstance(ref ByValArray<T> arr)
    {
        T[] orig = arr.array;
        return UnsafeTools.GetPointer(
            //Obtain the address from the reference.
            //It uses a lambda to minimize the chance of the reference
            //being moved around by the GC.
            out arr,
            ptr => {
                WeakReference<T[]> wref;
                T[] inst;
                if(Cache.TryGetValue(ptr, out wref) && wref.TryGetTarget(out inst))
                {
                    //An object is found on this address.
                    if(inst != orig)
                    {
                        //This address was overwritten with a new value,
                        //clone the instance.
                        inst = (T[])orig.Clone();
                        Cache[ptr] = new WeakReference<T[]>(inst);
                    }
                    return inst;
                }else{
                    //No object was found on this address,
                    //clone the instance.
                    inst = (T[])orig.Clone();
                    Cache[ptr] = new WeakReference<T[]>(inst);
                    return inst;
                }
            }
        );
    }

    //All subsequent methods should always update the state first.
    public T this[int index]
    {
        get{
            Update();
            return array[index];
        }
        set{
            Update();
            array[index] = value;
        }
    }

    public int Length{
        get{
            Update();
            return array.Length;
        }
    }

    public override bool Equals(object obj)
    {
        Update();
        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        Update();
        return base.GetHashCode();
    }

    public override string ToString()
    {
        Update();
        return base.ToString();
    }
}

var a = new ByValArray<int>(10);
a[5] = 11;
Console.WriteLine(a[5]); //11

var b = a;
b[5]++;
Console.WriteLine(b[5]); //12
Console.WriteLine(a[5]); //11

var c = a;
a = b;
Console.WriteLine(a[5]); //12
Console.WriteLine(c[5]); //11

正如您所看到的,这个值类型的行为就像每次复制对数组的引用时都将基础数组复制到新位置一样。

警告!!! 仅在自己的风险下使用此代码,并且最好永远不要在生产代码中使用。这种技术在许多层面上都是错误和邪恶的,因为它假定了某些不应该具有的身份。虽然这试图为这个结构体“强制执行”值类型语义(“目的证明手段”),但在几乎任何情况下,肯定有更好的解决方案来解决真正的问题。另请注意,尽管我已经尝试预见到这个类型可能会出现的任何可预见的问题,但仍可能存在这种类型会显示出相当意外行为的情况。


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