C# Автоматическая глубокая копия структуры

У меня есть структура, 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 структурой, потому что это был тип значения, и я не хотел, чтобы ссылки распространялись.

7 ответов

Решение

Среда выполнения выполняет быстрое копирование структур в память, и, насколько я знаю, невозможно ввести или принудительно применить собственную процедуру копирования для них. Вы могли бы представить свой собственный Clone метод или даже конструктор копирования, но вы не можете заставить их использовать их.

Лучше всего, если возможно, сделать вашу структуру неизменной (или неизменным классом) или изменить дизайн в целом, чтобы избежать этой проблемы. Если вы являетесь единственным потребителем API, то, возможно, вы можете просто сохранять бдительность.

Джон Скит (и другие) описал эту проблему, и хотя могут быть исключения, вообще говоря: изменяемые структуры являются злом. Могут ли структуры содержать поля ссылочных типов

Один простой способ сделать (глубокую) копию, хотя и не самый быстрый (потому что он использует отражение), это использовать 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;
        }
    }

Работает для классов и структур.

Struct копируется при правильной передаче? Так:

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

Применение:

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

В качестве обходного пути я собираюсь реализовать следующее.

В структуре есть 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 будет вести себя).

          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
   

Я думал о подобной проблеме, связанной с типами значений, и нашел "решение" этого. Видите ли, вы не можете изменить конструктор копирования по умолчанию в 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

Как видите, этот тип значения ведет себя точно так же, как если бы базовый массив копировался в новое место каждый раз, когда копируется ссылка на массив.

ПРЕДУПРЕЖДЕНИЕ!!! Используйте этот код только на свой страх и риск, и желательно никогда не в производственном коде. Эта техника неправильна и зла на многих уровнях, потому что она предполагает идентичность того, чего не должно быть. Хотя это пытается "навязать" семантику типов значений для этой структуры ("цель оправдывает средства"), безусловно, существуют более эффективные решения реальной проблемы практически в любом случае. Также обратите внимание, что, хотя я пытался предвидеть любые предсказуемые проблемы с этим, могут быть случаи, когда этот тип будет демонстрировать довольно неожиданное поведение.

Другие вопросы по тегам