C#自动深度复制struct

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

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

当我从现有的MyStruct对象创建一个新的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设为结构,因为它是值类型,我不希望引用传播。

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

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

Jon Skeet(以及其他人)已经描述了这个问题,虽然可以有例外,但一般来说:可变结构是邪恶的。 结构可以包含引用类型的字段

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

  static public T DeepCopy(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个方法可以修改BoolArray的内容。 不是在复制结构时创建数组,而是在进行更改它的调用时重新创建BoolArray,如下所示

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

虽然这对于涉及BoolArray的大量更改的任何用途都是不好的,但是我对结构的使用是很多复制,并且很少改变。 这只会在需要更改时更改对数组的引用。

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

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

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

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

任何这些方法的最大问题在于,虽然struct可以以这样的方式设计,即在允许有效复制的同时呈现一致的值类型语义,但是对结构代码的一瞥几乎不会那么明显(与普通的相比)旧数据结构,其中结构具有名为Foo的公共字段这一事实使得Foo将如何表现非常清楚)。

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

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

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

 public struct ByValArray { //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> Cache = new Dictionary>(); static T[] FindInstance(ref ByValArray 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 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(inst); } return inst; }else{ //No object was found on this address, //clone the instance. inst = (T[])orig.Clone(); Cache[ptr] = new WeakReference(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(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 

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

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