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 设为结构,因为它是值类型,并且我不希望引用传播。

4

6 回答 6

13

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

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

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

于 2012-07-05T02:09:21.933 回答
3

制作(深度)副本的一种简单方法,虽然不是最快的(因为它使用反射),但用于BinaryFormatter将原始对象序列化为 a MemoryStream,然后将其反序列化为MemoryStreamnew 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;
        }
    }

适用于类和结构。

于 2012-07-05T01:39:51.610 回答
2

作为一种解决方法,我将执行以下操作。

结构中有 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 的大量更改的任何用途都是不利的,但我对结构的使用是大量的复制,并且几乎没有更改。这只会在需要更改时更改对数组的引用。

于 2012-07-05T01:58:02.800 回答
1

为了避免奇怪的语义,任何包含可变引用类型字段的结构都必须做以下两件事之一:

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

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

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

任何这些方法的最大问题是,虽然struct可以以呈现一致的值类型语义同时允许有效复制的方式进行设计,但看一下结构的代码很难看出这一点(与普通旧的相比) -data 结构,其中结构有一个称为公共字段的事实Foo非常清楚Foo将如何表现)。

于 2012-07-05T20:31:49.363 回答
1

通过时复制结构吗?所以:

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

用法:

var clone = new AnyStruct ().Clone ();
于 2020-01-31T16:12:58.163 回答
0

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

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

在实践中,结构的地址可以从this引用中获得,因为它ref T在值类型的情况下很简单。我留下了从对我的库的引用中获取地址的方法,但是为此发出自定义 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

如您所见,此值类型的行为与每次复制对数组的引用时都将底层数组复制到新位置一样。

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

于 2017-08-08T23:29:55.647 回答