C#自动深层复制struct

时间:2012-07-05 01:35:53

标签: c# struct deep-copy default-copy-constructor

我有一个结构MyStruct,它有一个私有成员private bool[] boolArray;和一个方法ChangeBoolValue(int index, bool Value)

我有一个班级MyClass,其中有一个字段public MyStruct bools { get; private set; }

当我从现有的MyStruct对象创建一个新的MyStruct对象,然后应用方法ChangeBoolValue()时,两个对象中的bool数组都会被更改,因为引用而不是引用的引用被复制到新对象。 E.g:

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

有没有办法强制副本实现更深层次的副本,还是有办法实现这个不会出现同样的问题?

我特意将MyStruct设为结构,因为它是值类型,我不希望引用传播。

6 个答案:

答案 0 :(得分:9)

运行时执行结构的快速内存复制,据我所知,不可能为它们引入或强制执行自己的复制过程。您可以引入自己的Clone方法甚至是复制构造函数,但不能强制它们使用它们。

如果可能,最好的办法是使结构不可变(或不可变类)或重新设计以避免此问题。如果您是API的唯一消费者,那么也许您可以保持警惕。

Jon Skeet(和其他人)已经描述了这个问题,虽然可以有例外,但一般来说:可变结构是邪恶的。 Can structs contain fields of reference types

答案 1 :(得分: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;
        }
    }

适用于类和结构。

答案 2 :(得分:1)

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

结构中有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的大量更改的任何用途都是不好的,但我对结构的使用是很多复制,而且变化很小。这只会在需要更改时更改对数组的引用。

答案 3 :(得分:1)

为了避免奇怪的语义,任何包含可变引用类型字段的结构必须执行以下两种操作之一:

  1. 应该清楚地表明,从它的角度来看,该领域的内容不是“保持”一个对象,而只是为了识别一个对象。例如,`KeyValuePair&lt; String,Control&gt;`将是一个完全合理的类型,因为尽管`Control`是可变的,但由这种类型引用的控件的标识将是不可变的。
  2. 可变对象必须是由值类型创建的对象,永远不会暴露在它之外。此外,必须在对对象的引用存储到结构的任何字段之前执行将对不可变对象执行的任何突变。

正如其他人所指出的那样,允许结构体模拟数组的一种方法是保存数组,并在修改元素时随时生成该数组的新副本。当然,这样的事情会非常缓慢。另一种方法是添加一些逻辑来存储最后几个突变请求的索引和值;每次尝试读取数组时,检查该值是否是最近写入的值之一,如果是,则使用结构中存储的值而不是数组中的值。一旦填充了结构中的所有“槽”,就制作一个数组副本。如果更新到达许多不同的元素,这种方法充其量只能“仅”提供恒定的速度而不是重新生成数组,但如果绝大多数更新都会触及少量元素,则可能会有所帮助。

另一种方法是,更新可能具有较高的特殊集中度,但是为了使它们完全适合结构,需要使用太多元素来保持对“主”数组的引用,以及“更新”数组以及一个整数,表示“updates”数组所代表的主数组的哪一部分。更新通常需要重新生成“更新”数组,但这可能比主数组小得多;如果 “updates”数组变得太大,主数组可以通过其中包含的“updates”数组所代表的更改进行重新生成。

这些方法中的任何一个问题的最大问题在于,虽然可以设计struct以便在允许高效复制的同时呈现一致的值类型语义,但只需查看结构的代码即可显而易见(与普通旧数据结构相比,结构具有名为Foo的公共字段的事实使得Foo的行为非常清楚。

答案 4 :(得分:0)

我在考虑与价值类型相关的类似问题,并找到了一个解决方案&#34;对此。你知道,你不能更改C#中的默认复制构造函数,就像你在C ++中一样,因为它的目的是轻量级和无副作用。但是,您可以做的是等到实际访问该结构,然后检查它是否已被复制。

这个问题是,与引用类型不同,结构没有真正的身份;只有按价值平等。但是,它们仍然必须存储在内存中的某个位置,并且此地址可用于识别(尽管是暂时的)值类型。 GC在这里是一个问题,因为它可以移动对象,因此可以更改结构所在的地址,因此您必须能够处理它(例如,将结构的数据设为私有)。

实际上,结构的地址可以从this引用获得,因为在值类型的情况下它是一个简单的ref T。我离开means以从我的库的引用中获取地址,但为此发出自定义CIL非常简单。在这个例子中,我创建了一个基本上是byval数组的东西。

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

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

警告!!! 使用此代码,风险自负,最好不要在生产代码中使用。这种技术在很多层面都是错误和邪恶的,因为它假定了不应该拥有它的东西的身份。虽然这试图强制执行#34;这个结构的值类型语义(&#34;结束证明了手段&#34;),对于几乎任何情况下的实际问题,肯定有更好的解决方案。另请注意,虽然我已经尝试预见到任何可预见的问题,但可能会出现这种类型会出现意外行为的情况。

答案 5 :(得分:0)

传递正确时复制结构吗?所以:

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

用法:

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